packages feed

curl-aeson 0.0.4 → 0.1.0.0

raw patch · 4 files changed

+378/−121 lines, 4 filesdep +bytestringdep −utf8-stringdep ~aesondep ~curldep ~text

Dependencies added: bytestring

Dependencies removed: utf8-string

Dependency ranges changed: aeson, curl, text

Files

+ README.md view
@@ -0,0 +1,78 @@+<!-- -*- mode: markdown; coding: utf-8 -*- -->++# curl-aeson library for Haskell++This is a library for communicating with JSON over HTTP connection.+It supports rich set of HTTP connectivity features provided by+[curl](https://github.com/galoisinc/curl) library combined to the+performance and elegance of [aeson](https://github.com/bos/aeson).++Author: Joel Lehtonen <joel.lehtonen+curlaeson@iki.fi>++This library is at its best when communicating with simple,+non-standardized JSON interfaces. If you are implementing JSON-RPC+compliant client or server, take a look at+[another library](http://hackage.haskell.org/package/jmacro-rpc).++## Example++In this example we fetch latest bid and ask values from a Bitcoin+exchange using+[their public API](https://github.com/paytunia/api-documentation#read-the-ticker):++```haskell+{-# LANGUAGE OverloadedStrings #-}+import Control.Monad+import Data.Aeson+import Network.Curl.Aeson++ticker :: IO (Double,Double)+ticker = curlAesonGetWith p "https://bitcoin-central.net/api/v1/ticker/eur"+  where+    p (Object o) = do+      bid <- o .: "bid"+      ask <- o .: "ask"+      return (bid,ask)+    p _ = mzero+```++You may find more examples from package documentation.++## Installation++### On Ubuntu and Debian++Starting from Ubuntu 12.04 and Debian wheezy, all dependencies are+found from repositories:++    sudo apt-get install libghc-aeson-dev libghc-curl-dev cabal-install++Then just install this:++    cabal install++### Other++Install and configure+[Haskell Platform](http://www.haskell.org/platform/). Then, fetch all the+requirements and install this library by running:++    cabal update+	cabal install++## License++Following the convention of Haskell community, this library is+licensed under the terms of+[BSD 3-clause license](https://en.wikipedia.org/wiki/BSD_licenses#3-clause_license_.28.22Revised_BSD_License.22.2C_.22New_BSD_License.22.2C_or_.22Modified_BSD_License.22.29).+Personally I prefer GPLv3, but this library is simple enough to be+released with non-[copyleft](https://en.wikipedia.org/wiki/Copyleft)+license.++The license text is included in LICENSE file.++## Contact++I'm not an expert in doing software packages, so feel free to correct+me by sending pull requests. Also, I would like to hear if you have+found my library to be useful.
curl-aeson.cabal view
@@ -1,38 +1,46 @@-name:          curl-aeson-version:       0.0.4-synopsis:      Communicate with HTTP service using JSON -description:   A library for communicating with JSON over HTTP connection.-	       Supports rich set of HTTP connectivity features provided by-	       libcurl combined to the performance and elegancy of aeson.-	       .-	       All HTTP methods are supported. Instances of 'ToJSON' and-	       'FromJSON' typeclasses can be transferred via this library.-	       Session cookies and other HTTP options may be passed to libcurl-	       if needed.-	       .-	       This library is at its best when communicating with simple,-	       non-standardized JSON interfaces. If you are implementing-	       JSON-RPC compliant client or server, take a look at-	       <http://hackage.haskell.org/package/jmacro-rpc>.-category:      Network, Web, JSON-license:       BSD3-license-file:  LICENSE-author:        Joel Lehtonen-maintainer:    joel.lehtonen+curlaeson@iki.fi-homepage:      https://github.com/zouppen/haskell-curl-aeson-build-type:    Simple-cabal-version: >= 1.6--library-  hs-source-dirs:  src-  exposed-modules: Network.Curl.Aeson-  build-depends:-    aeson >= 0.6.0.0,-    base >= 4 && < 5,-    curl >= 1.3.7,-    text,-    utf8-string >= 0.3+cabal-version:       >=1.10+name:                curl-aeson+version:             0.1.0.0+synopsis:            Communicate with HTTP service using JSON +description:         A library for communicating with JSON over HTTP+                     connection. Supports rich set of HTTP connectivity+                     features provided by libcurl combined to the+                     performance and elegancy of aeson.+                     .+                     All HTTP methods are supported. Instances of 'ToJSON'+                     and 'FromJSON' typeclasses can be transferred via+                     this library. Session cookies and other HTTP options+                     may be passed to libcurl if needed.+                     .+                     This library is at its best when communicating with+                     simple, non-standardized JSON interfaces. If you are+                     implementing JSON-RPC compliant client or server,+                     take a look at+                     <http://hackage.haskell.org/package/jmacro-rpc>.+category:            Network, Web, JSON+license:             BSD3+license-file:        LICENSE+author:              Joel Lehtonen+maintainer:          joel.lehtonen+curlaeson@iki.fi+homepage:            https://github.com/zouppen/haskell-curl-aeson+bug-reports:         https://github.com/zouppen/haskell-curl-aeson/issues+build-type:          Simple+extra-source-files:  README.md  source-repository head-  type: git-  location: git://github.com/zouppen/haskell-curl-aeson.git+  type:              git+  location:          https://github.com/zouppen/haskell-curl-aeson.git++library+  exposed-modules:   Network.Curl.Aeson+  other-modules:     Network.Curl.Aeson.Internal+  other-extensions:  RecordWildCards, OverloadedStrings+  build-depends:     base ==4.*,+                     aeson >= 0.6,+                     curl >=1.3 && <1.4,+                     -- Text and ByteString dependencies should be+                     -- in-line with aeson dependencies+                     bytestring >=0.10.8.1 && <0.12,+                     text >=1.2.3.0 && <1.3 || >=2.0 && <2.1+  hs-source-dirs:    src+  default-language:  Haskell2010
src/Network/Curl/Aeson.hs view
@@ -1,92 +1,187 @@-{-# LANGUAGE DeriveDataTypeable, RecordWildCards #-}+{-# LANGUAGE RecordWildCards #-} -- | -- Module    : Network.Curl.Aeson--- Copyright : (c) 2013, Joel Lehtonen+-- Copyright : (c) 2013-2022, Joel Lehtonen -- License   : BSD3 -- -- Maintainer: Joel Lehtonen <joel.lehtonen+curlaeson@iki.fi> -- Stability : experimental -- Portability: portable ----- Functions for communicating with JSON over HTTP connection.+-- Functions for communicating with JSON over HTTP, HTTPS, or any+-- protocol supported by [cURL](https://curl.se/).  module Network.Curl.Aeson        ( -- * How to use this library          -- $use          -         -- * Sending HTTP request+         -- * cURL requests with JSON payload and response          curlAesonGet        , curlAesonGetWith-       , curlAeson-         -- * Helper functions+       , curlAesonCustom+       , curlAesonCustomWith+         -- * Generic cURL request+       , curlAesonRaw+         -- * Helpers for working with raw requests+       , jsonPayload+       , binaryPayload+       , binaryResponse+       , valueResponse+       , jsonResponse+         -- * Other helper functions        , cookie        , rawJson        , (...)        , noData+         -- * Types+       , Payload(..)+       , ResponseParser          -- * Exception handling        , CurlAesonException(..)+         -- * Deprecated functions+       , curlAeson        ) where  import Control.Exception import Control.Monad import Data.Aeson import Data.Aeson.Types-import Data.ByteString.Lazy.UTF8 (fromString,toString)+import Data.ByteString.Lazy (ByteString)+import qualified Data.ByteString.Lazy as B import Data.Maybe import Data.Text (Text) import Data.Typeable import Network.Curl+import Network.Curl.Aeson.Internal  -- | Shorthand for doing just a HTTP GET request and parsing the output to--- any FromJSON instance.-curlAesonGet :: (FromJSON a) => URLString -> IO a-curlAesonGet = curlAesonGetWith parseJSON+-- any 'FromJSON' instance.+curlAesonGet :: (FromJSON a)+  => URLString -- ^ Request URL+  -> IO a      -- ^ Received and parsed data+curlAesonGet url = curlAesonCustom [] "GET" url noData  -- | Shorthand for doing just a HTTP GET request and parsing the -- output with given parser /p/.-curlAesonGetWith :: (Value -> Parser a) -> URLString -> IO a-curlAesonGetWith p url = curlAeson p "GET" url [] noData+curlAesonGetWith+  :: (Value -> Parser a) -- ^Aeson parser for response. Use 'pure' if+                         -- you want it in AST format.+  -> URLString           -- ^Request URL+  -> IO a                -- ^Received and parsed data+curlAesonGetWith p url = curlAesonCustomWith p [] "GET" url noData --- | Send single HTTP request.+-- | Send a single HTTP request and a custom parser. --  -- The request automatically has @Content-type: application/json@ -- header if you pass any data. This function is lenient on response--- content type: everything is accepted as long as it is parseable--- with 'decode'. HTTP payload is expected to be UTF-8 encoded.+-- content type; everything is accepted as long as it is valid JSON+-- and parseable with your supplied parser. --  -- If you need authentication, you need to pass session cookie or -- other means of authentication tokens via 'CurlOption' list.-curlAeson ::-  (ToJSON a)-  => (Value -> Parser b) -- ^ Parser for response. Use 'parseJSON' if-                         -- you like want to use FromJSON instance or+curlAesonCustomWith+  :: (ToJSON a)+  => (Value -> Parser b) -- ^ Aeson parser for response. Use                          -- 'pure' if you want it in AST format.+  -> [CurlOption]        -- ^ Session cookies, or other cURL+                         -- options. Use 'mempty' if you don't need+                         -- any.   -> String              -- ^ Request method   -> URLString           -- ^ Request URL-  -> [CurlOption]        -- ^ Session cookies, or other cURL-                         -- options. Use empty list if you don't need+  -> Maybe a             -- ^ JSON data to send, or 'Nothing' when+                         -- sending request without any content.+  -> IO b                -- ^ Received and parsed data+curlAesonCustomWith parser opts method url maybeValue =+  curlAesonRaw+  (\x -> eitherDecode x >>= parseEither parser)+  opts method url+  (maybeValue >>= jsonPayload)++{-# DEPRECATED curlAeson "Use customAesonCustomWith instead" #-}+-- |See type of 'curlAesonCustomWith'.+curlAeson :: ToJSON a => (Value -> Parser b) -> String -> URLString -> [CurlOption] -> Maybe a -> IO b+curlAeson parser method url opts = curlAesonCustomWith parser opts method url++-- | Send a single cURL request.+--+-- The request automatically has @Content-type: application/json@+-- header if you pass any data. This function is lenient on response+-- content type; everything is accepted as long as 'parseJSON'+-- succeeds.+--+-- If you need authentication, you need to pass session cookie or+-- other means of authentication tokens via 'CurlOption' list.+curlAesonCustom ::+  (ToJSON a, FromJSON b)+  => [CurlOption]        -- ^ Session cookies, or other cURL+                         -- options. Use 'mempty' if you don't need                          -- any.-  -> Maybe a             -- ^ JSON data to send, or Nothing when+  -> String              -- ^ Request method+  -> URLString           -- ^ Request URL+  -> Maybe a             -- ^ JSON data to send, or 'Nothing' when                          -- sending request without any content.-  -> IO b                -- ^ Received JSON data-curlAeson parser method url extraOpts maybeContent = do-  (curlCode,received) <- curlGetString url curlOpts-  when (curlCode /= CurlOK) $ throw CurlAesonException{errorMsg="HTTP error",..}-  let ast = case decode $ fromString received of-        Nothing -> throw CurlAesonException{errorMsg="JSON parsing failed",..}-        Just x  -> x-  return $ case parseEither parser ast of-    Left errorMsg -> throw CurlAesonException{..}-    Right x -> x-  where-    curlOpts = commonOpts++dataOpts++extraOpts-    commonOpts = [CurlCustomRequest method]-    dataOpts = case maybeContent of-      Nothing -> []-      Just a  -> [CurlPostFields [toString $ encode a]-                 ,CurlHttpHeaders ["Content-type: application/json"]-                 ]+  -> IO b                -- ^ Received and parsed data+curlAesonCustom opts method url maybeValue =+  curlAesonRaw eitherDecode opts method url+  (maybeValue >>= jsonPayload) +-- |The most flexible function we have here.+--+-- Sends raw cURL request with a possible payload and collects the+-- output. This function is binary safe so it works with other media+-- types than JSON and supports binary upload and JSON response+-- (common case with media uploads).+--+-- When /payload/ is given, cURL options 'CurlReadFunction',+-- 'CurlUpload' and 'CurlInFileSizeLarge' are set and HTTP header+-- Content-Type is appended implicitly.+curlAesonRaw+  :: ResponseParser a    -- ^ Parser function for the response such as 'eitherDecode'+  -> [CurlOption]        -- ^ Extra curl options.+  -> String              -- ^ Request method+  -> URLString           -- ^ Request URL+  -> Maybe Payload       -- ^ Request body payload, if any.+  -> IO a                -- ^ Received and parsed data+curlAesonRaw parser userOpts method url maybePayload = do+  -- Prepare the upload+  putOpts <- case maybePayload of+    Nothing -> pure userOpts+    Just Payload{..} -> do+      readFunc <- mkReadFunctionLazy payload+      pure $ CurlReadFunction readFunc :+             CurlUpload True :+             CurlInFileSizeLarge (fromIntegral $ B.length payload) :+             mergeHeaders [ "Content-Type: " <> contentType ] userOpts+  -- Add request method+  let curlOpts = CurlCustomRequest method : putOpts+  -- Perform the request+  (curlCode, received) <- curlGetString_ url curlOpts+  when (curlCode /= CurlOK) $+    throwIO CurlAesonException{parseError = Nothing, ..}+  -- Trying to parse+  case parser received of+    Left e  -> throwIO CurlAesonException{parseError = Just e, ..}+    Right x -> pure x++-- |Internal tool to merge headers in a cURL option list+mergeHeaders :: [String] -> [CurlOption] -> [CurlOption]+mergeHeaders acc [] = [CurlHttpHeaders acc]+mergeHeaders acc ((CurlHttpHeaders x):xs) = mergeHeaders (acc <> x) xs+mergeHeaders acc (x:xs) = x:mergeHeaders acc xs ++-- |Convert a value to JSON payload. This never returns Nothing.+jsonPayload :: ToJSON a => a -> Maybe Payload+jsonPayload a = Just Payload{..}+  where payload = encode a+        contentType = "application/json"++-- |Just a shortcut for defining binary payloads of given media+-- type. This never returns Nothing.+binaryPayload :: String        -- ^Media type (MIME)+              -> ByteString    -- ^Data+              -> Maybe Payload -- ^Payload+binaryPayload a b = Just $ Payload a b+ -- | Helper function for writing parsers for JSON objects which are -- not needed to be parsed completely. --@@ -95,7 +190,7 @@ -- @OverloadedStrings@ language extension which enables 'Text' values -- to be written as string literals. ----- @p ('Object' o) = 'pure' obj'...'\"glossary\"'...'\"title\"+-- @p ('Data.Aeson.Types.Internal.Object' o) = 'pure' o'...'\"glossary\"'...'\"title\" --p _ = 'mzero' -- @ (...) :: FromJSON b@@ -111,65 +206,92 @@  -- | Single cookie of given key and value. cookie :: String -> String -> CurlOption-cookie key value = CurlCookie $ key++"="++value+cookie key value = CurlCookie $ key <> "=" <> value  -- | Useful for just giving the JSON as string when it is static -- anyway and doesn't need to be programmatically crafted.-rawJson :: String -> Maybe Value-rawJson = decode . fromString+rawJson :: ByteString -> Maybe Value+rawJson = decode --- |To avoid ambiguity in type checker you may pass this value instead--- of Nothing to 'curlAeson'.+-- |Useful with 'curlAesonRaw' when you just need to take the binary output.+binaryResponse :: ResponseParser ByteString+binaryResponse = Right++-- |Useful with 'curlAesonRaw' when you just want to get the JSON Value+valueResponse :: ResponseParser Value+valueResponse = eitherDecode++-- |Just a friendly name for 'eitherDecode'. Can be used with+-- 'curlAesonRaw' for deserializing the response using 'FromJSON'+-- instance.+jsonResponse :: FromJSON a => ResponseParser a+jsonResponse = eitherDecode++-- |To avoid type ambiguity you may pass this value instead+-- of Nothing to 'curlAesonCustom'. noData :: Maybe Value noData = Nothing +-- |Holds the payload for raw sender.+data Payload = Payload+  { contentType :: String      -- ^Content media type (MIME)+  , payload     :: ByteString  -- ^Data+  } deriving (Show)+ -- | This exception is is thrown when Curl doesn't finish cleanly or -- the parsing of JSON response fails.-data CurlAesonException = CurlAesonException { url      :: URLString-                                             , curlCode :: CurlCode-                                             , curlOpts :: [CurlOption]-                                             , received :: String-                                             , errorMsg :: String-                                             } deriving (Show, Typeable)+data CurlAesonException = CurlAesonException+  { url        :: URLString    -- ^The request URI+  , curlCode   :: CurlCode     -- ^Curl return code+  , curlOpts   :: [CurlOption] -- ^Curl options set+  , received   :: ByteString   -- ^Received raw data from the+                               -- server. Before version 0.1 the type+                               -- was 'Prelude.String'.+  , parseError :: Maybe String -- ^Parse error, if it failed during parse.+  } deriving (Show)+ instance Exception CurlAesonException +-- |Parser type from response to your data. Normally: 'eitherDecode'+type ResponseParser a = ByteString -> Either String a+ -- $use ----- To get bid and ask levels as a pair from a Bitcoin exchange using its public--- API:+-- Let\'s simulate a service by creating a file @\/tmp\/ticker.json@+-- with the following content: ----- @{-\# LANGUAGE OverloadedStrings #-}---import Control.Monad---import Data.Aeson---import Network.Curl.Aeson+-- > {"bid":3,"ask":3.14} -----ticker :: 'IO' ('Double','Double')---ticker = 'curlAesonGetWith' p \"https:\/\/bitcoin-central.net\/api\/v1\/ticker\/eur\"---  where---    p ('Object' o) = do---      bid <- o '.:' \"bid\"---      ask <- o '.:' \"ask\"---      'return' (bid,ask)---    p _ = 'mzero'--- @--- --- The same as above, but we define our own data type which is an--- instance of FromJSON:--- --- @{-\# LANGUAGE OverloadedStrings #-}---import Control.Applicative---import Control.Monad---import Data.Aeson---import Network.Curl.Aeson+-- This example shows how to hand-craft the parser for the bid and ask+-- values: -----data Ticker = Ticker { bid :: 'Double'---                     , ask :: 'Double'---                     } deriving ('Show')+-- > {-# LANGUAGE OverloadedStrings #-}+-- > import Control.Monad+-- > import Data.Aeson+-- > import Network.Curl.Aeson+-- >+-- > ticker :: IO (Double,Double)+-- > ticker = curlAesonGetWith p "file:///tmp/ticker.json"+-- >   where+-- >     p (Object o) = do+-- >       bid <- o .: "bid"+-- >       ask <- o .: "ask"+-- >       return (bid,ask)+-- >     p _ = mzero -----instance 'FromJSON' Ticker where---    parseJSON ('Object' o) = Ticker '<$>' o '.:' \"bid\" '<*>' o '.:' \"ask\"---    parseJSON _ = 'mzero'+-- The same as above, but we define our own data type which is an+-- instance of 'FromJSON': -----ticker :: 'IO' Ticker---ticker = 'curlAesonGet' \"https:\/\/bitcoin-central.net\/api\/v1\/ticker\/eur\"--- @+-- > {-# LANGUAGE DeriveGeneric #-}+-- > import GHC.Generics+-- > import Data.Aeson+-- > import Network.Curl.Aeson+-- >+-- > data Ticker = Ticker { bid :: Double+-- >                      , ask :: Double+-- >                      } deriving (Generic, Show)+-- >+-- > instance FromJSON Ticker+-- >+-- > ticker :: IO Ticker+-- > ticker = curlAesonGet "file:///tmp/ticker.json"
+ src/Network/Curl/Aeson/Internal.hs view
@@ -0,0 +1,49 @@+{-# LANGUAGE OverloadedStrings #-}+-- |+-- Module    : Network.Curl.Aeson+-- Copyright : (c) 2022, Joel Lehtonen+-- License   : BSD3+--+-- Maintainer: Joel Lehtonen <joel.lehtonen+curlaeson@iki.fi>+-- Stability : experimental+-- Portability: portable+--+-- Internal support functions to get ByteString payload out of+-- libcurl.+module Network.Curl.Aeson.Internal where++import Data.IORef+import qualified Data.ByteString as B+import qualified Data.ByteString.Unsafe as B+import qualified Data.ByteString.Internal as B+import qualified Data.ByteString.Lazy as BL+import Foreign.Ptr+import Network.Curl.Opts (ReadFunction)++-- |Creates ReadFunction which feeds strict ByteString.+mkReadFunction :: B.ByteString -> IO ReadFunction+mkReadFunction bs = feeder <$> newIORef [bs]++-- |Creates ReadFunction which feeds lazy ByteString.+mkReadFunctionLazy :: BL.ByteString -> IO ReadFunction+mkReadFunctionLazy bs = feeder <$> newIORef (BL.toChunks bs)++-- |ReadFunction for curl to feed in the payload+feeder :: IORef [B.ByteString] -> ReadFunction+feeder input dst size nitems _ = do+  -- Take next chunk+  chunk <- atomicModifyIORef input $ takeChunk $ fromIntegral $ size*nitems+  -- Now doing the hard copying+  B.unsafeUseAsCStringLen chunk rawCopy+  pure $ Just $ fromIntegral $ B.length chunk+  where rawCopy (src, srcLen) = B.memcpy (castPtr dst) (castPtr src) srcLen++-- |Takes chunk of size 1 to /len/ bytes. In case the chunk list is+-- empty, it returns zero-length string. May give unnecessarily short+-- chunks in case of small chunks way smaller than buffer.+takeChunk :: Int -> [B.ByteString] -> ([B.ByteString], B.ByteString)+takeChunk len [] = ([], "")+takeChunk len ("":xs) = takeChunk len xs+takeChunk len (chunk:xs) =+  let (now, later) = B.splitAt len chunk+  in (later:xs, now)