How to process HttpResponseBody in Haskell req? - haskell

I have an application that gets JSON response from an API and I'm trying to get a specific key from that response. The working code looks as below:
{-# LANGUAGE OverloadedStrings #-}
module Main (main) where
import Data.Aeson
import Network.HTTP.Req
tinyUIDRequest :: String -> Req (JsonResponse Object)
tinyUIDRequest url = let payload = object [ "url" .= url ]
in req
POST
(https "tinyuid.com" /: "api" /: "v1" /: "shorten") (
ReqBodyJson payload)
jsonResponse
mempty
main :: IO ()
main = do
response <- runReq defaultHttpConfig (tinyUIDRequest "http://google.com")
let body = responseBody response
print body
And the function to process the response should look similar to:
tinyUIDResponseHandler :: HttpResponseBody (JsonResponse Object) -> String
tinyUIDResponseHandler r = case lookup "result_url" r of
Just url -> url
Nothing -> ""
unfortunately I'm not able to figure out how to transform HttpResponseBody (JsonResponse Object) to something like hashmap/list and find a proper key. I was checking the documentation and req source code but maybe, as a beginner in Haskell, I don't know exactly what to look for to understand the internals of the response type.

HttpResponseBody is a red herring. It's an associated type, and HttpResponseBody (JsonResponse a) is the same as just a, so HttpResponseBody (JsonResponse Object) is really just Object. As such, once you have body, req's job is done, and it's now just aeson that you need to be concerned with.
Since body is an Object and not a list of tuples, you can't use Prelude's lookup on it. If your Aeson version is older than 2, then Object is just HashMap Text Value, so do this instead:
import Data.Text
import qualified Data.HashMap.Strict as HM
tinyUIDResponseHandler :: Object -> Text
tinyUIDResponseHandler r = case HM.lookup "result_url" r of
Just (String url) -> url
_ -> ""
If your Aeson version is 2 or newer, then Object is KeyMap Value, so do this instead:
import Data.Text
import qualified Data.Aeson.KeyMap as AKM
tinyUIDResponseHandler :: Object -> Text
tinyUIDResponseHandler r = case AKM.lookup "result_url" r of
Just (String url) -> url
_ -> ""
In either case, tinyUIDResponseHandler body will then be the value you want, so for example, you could do print (tinyUIDResponseHandler body).

Related

In the function extactValues I have to return WeatherValues

I have to ask how to fix this
Couldn't match type ‘Response String’ with ‘[Char]’
Expected type: String
Actual type: Response String
and this
Couldn't match type ‘Response String’ with ‘[Char]’
Expected type: String
Actual type: Response String
in
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE DeriveGeneric #-}
module PrepareAnswer where
import Data.Text
import Data.Aeson
import Network.HTTP.Client
import GHC.Generics
import Control.Monad
import Data.Maybe
import Data.Aeson.Encode.Pretty
import Data.ByteString
import AskWeather
import Data.Aeson.Types
import Data.ByteString.Char8
data TempUnits = Celsiuses
| Farenheits deriving (Generic, Show)
data WeatherValues = WeatherValues
{ temp_min :: Text
, temp_max :: Text
} deriving (Show)
extractValues :: Response String -> WeatherValues
extractValues response =
let rawJSON = response
result = decodeStrict (Data.ByteString.Char8.pack rawJSON) :: Maybe Object
in case result of
Nothing -> "Invalid JSON!"
Just info -> WeatherValues temp_min (getTempMin info)
getTempMin :: Object -> Text
getTempMin info =
case parseMaybe extractTempMin info of
Nothing -> ""
Just info -> info
where
extractTempMin = \info -> info .: "main"
>>=
\mainInfo -> mainInfo .: "temp_min"
Let's focus down on the part of the code that's giving you this error.
You know how to convert a String to a Maybe Object. I'm going to extract that to its own function so we can talk about types.
convert :: String -> Maybe Object
convert rawJSON = decodeStrict (Data.ByteString.Char8.pack rawJSON)
However in extractValues what you're given is a Response String, not a String. These are fundamentally different types, just like Maybe Int and Int are.
If you try to do convert response, the compiler is going to complain at you because of this type mismatch, so we need to do something else.
What something else is depends a lot on the API for Response. I assume it's defined in AskWeather, so I can only guess, but here's some common patterns that might apply, depending on what that API is.
If there's a function in AskWeather with the type Response a -> a or Response String -> String, then we need to call that function on the response to extract the String from the Response String.
withExtract :: (Response String -> String) -> Response String -> Maybe Object
withExtract extract response = convert (extract response)
If Response is a Functor, there may not be a way to extract a String from a Response String, but we can use fmap to change the String it contains into a different type. This means the return value will still be wrapped in a Response
asFunctor :: Functor Response => Response String -> Response (Maybe Object)
asFunctor response = fmap convert response
According to Network.HTTP.Client, there is a function
responseBody :: Response body -> body
Note that body above can be any type you want, so you can use this to convert a Response String into a String, as needed.
(I now see that this was already suggested in a comment by #jkeuhlen, which pointed to the relevant documentation.)

Parsing iCalendar format

I need to parse iCalendar format, which is basically what is used by Google Calendar and almost all calendar apps.
I have found this package iCalendar Hackage
But I cannot figure out how to use the parseICalendar function in this package, if someone can tell me what I am doing wrong, it would be great.
Mainly I cannot figure out how to construct an argument for the Type DecodingFunctions
parseICalendar :: DecodingFunctions
-> FilePath -- ^ Used in error messages.
-> ByteString
-> Either String ([VCalendar], [String])
My effort:
module CalendarReader
( getCalendar
, getSummary
) where
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Lazy as B -- package "bytestring"
import qualified Text.ICalendar as ICal -- package "iCalendar"
import qualified Data.Map as Map -- package "containers"
import Network.HTTP.Simple -- package "http-conduit"
import qualified Time -- local module
import Constants
getCalendar :: IO B.ByteString
getCalendar = do
request <- parseRequest $ "GET" ++ calendarURL
response <- httpLBS request
return $ getResponseBody response
getSummary :: B.ByteString -> Time.DateTime -> Int -> String
getSummary cal dateTime dayOffset = summary
where
summary = "Event Summary"
((ICal.VCalendar { ICal.vcEvents = vcEvents' }), _) = ICal.parseICalendar ?missingArgument? logFile cal
DecodingFunctions is supposed to contains a function to convert your ByteString (binary array) to a Text (representing a string of unicode characters) and one to do the same to a case-insensitive representation (for comparison purpose I suppose). If your iCalendar is "normal" and is encoded in utf-8, you can simply use the Default instance of DecodingFunctions :
parseICalendar def logFile cal
(don't forget to import def from somewhere)
If your iCalendar is not in Utf-8, you'll have to use a decode... function from Data.Text.Lazy.Encoding and mk from Data.CaseInsensitive. For Utf16 you would have :
decodings = DecodingFunctions decodeUtf16LE (mk . decodeUtf16LE)
with the right imports.

Replace nested pattern matching with "do" for Monad

I want to simplify this code
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import Network.HTTP.Types
import Data.Text
getJSON :: String -> IO (Either String Value)
getJSON url = eitherDecode <$> simpleHttp url
--------------------------------------------------------------------
maybeJson <- getJSON "abc.com"
case maybeJson of
Right jsonValue -> case jsonValue of
(Object jsonObject) ->
case (HashMap.lookup "key123" jsonObject) of
(Just (String val)) -> Data.Text.IO.putStrLn val
_ -> error "Couldn't get the key"
_ -> error "Unexpected JSON"
Left errorMsg -> error $ "Error in parsing: " ++ errorMsg
by using do syntax for Monad
maybeJson <- getJSON "abc.com/123"
let toPrint = do
Right jsonValue <- maybeJson
Object jsonObject <- jsonValue
Just (String val) <- HashMap.lookup "key123" jsonObject
return val
case toPrint of
Just a -> Data.Text.IO.putStrLn a
_ -> error "Unexpected JSON"
And it gave me 3 errors:
src/Main.hs:86:19:
Couldn't match expected type `Value'
with actual type `Either t0 (Either String Value)'
In the pattern: Right jsonValue
In a stmt of a 'do' block: Right jsonValue <- maybeJson
src/Main.hs:88:19:
Couldn't match expected type `Value' with actual type `Maybe Value'
In the pattern: Just (String val)
In a stmt of a 'do' block:
Just (String val) <- HashMap.lookup "key123" jsonObject
src/Main.hs:88:40:
Couldn't match type `Maybe' with `Either String'
Expected type: Either String Value
Actual type: Maybe Value
Even when I replace
Just (String val) <- HashMap.lookup "key123" jsonObject
with
String val <- HashMap.lookup "key123" jsonObject
I'm getting another similar error about Either:
Couldn't match type `Maybe' with `Either String'
Expected type: Either String Value
Actual type: Maybe Value
In the return type of a call of `HashMap.lookup'
In a stmt of a 'do' block:
String val <- HashMap.lookup "key123" jsonObject
How do I fix those errors?
You can't easily simplify that into a single block of do-notation, because each case is matching over a different type. The first is unpacking an either, the second a Value and the third a Maybe. Do notation works by threading everything together through a single type, so it's not directly applicable here.
You could convert all the cases to use the same monad and then write it all out in a do-block. For example, you could have helper functions that do the second and third pattern match and produce an appropriate Either. However, this wouldn't be very different to what you have now!
In fact, if I was going for this approach, I'd just be content to extract the two inner matches into their own where variables and leave it at that. Trying to put the whole thing together into one monad just confuses the issue; it's just not the right abstraction here.
Instead, you can reach for a different sort of abstraction. In particular, consider using the lens library which has prisms for working with nested pattern matches like this. It even supports aeson nateively! Your desired function would look something like this:
decode :: String -> Maybe Value
decode json = json ^? key "key123"
You could also combine this with more specific prisms, like if you're expecting a string value:
decode :: String -> Maybe String
decode json = json ^? key "key123" . _String
This takes care of parsing the json, making sure that it's an object and getting whatever's at the specified key. The only problem is that it doesn't give you a useful error message about why it failed; unfortunately, I'm not good enough with lens to understand how to fix that (if it's possible at all).
So every line in a do expression for a Monad must return a value in that Monadic type. Monad is a typeclass here, not a type by itself. So putting everything in a do Monad is not really a sensible statement.
You can try your code with everything wrapped in a Maybe monad.
Assuming you've fetched your JSON value:
{-# LANGUAGE OverloadedStrings #-}
import Data.Aeson
import Network.HTTP
import qualified Data.Map as M
import Control.Applicative
import qualified Data.HashMap.Strict as HM
--------------------------------------------------------------------
main = do
maybeJson <- return $ toJSON (M.fromList [("key123","value")] :: M.Map String String)
ioVal <- return $ do -- The Maybe monad do expression starts here
maybeJson <- Just maybeJson
jsonObject <- case maybeJson of
Object x -> Just x
_ -> Nothing
val <- HM.lookup "key123" jsonObject
return val
putStrLn $ show ioVal
Once we start working in the Maybe monad, every expression must return a Maybe Something value. The way the Maybe monad works is that anything that is a Just something comes out as a pure something that you can work with, but if you get a Nothing, the rest of the code will be skipped and you'll get a Nothing.
This property of falling through is unique to the Maybe monad. Different monads behave differently.
You should read up more about Monads and the IO monad here: http://www.haskell.org/haskellwiki/Introduction_to_IO
You should read more about monads and what they really help you do:
http://learnyouahaskell.com/a-fistful-of-monads
(You should work through the previous chapters and then get to this chapter. Once you do, you'll have a pretty solid understanding of what is happening).
I also think your HTTP request is screwed up. Here's an example of a POST request that you can use.
import qualified Network.HTTP as H
main = do
postData <- return $ H.urlEncodeVars [("someVariable","someValue")]
request <- return $ H.postRequestWithBody "http://www.google.com/recaptcha/api/verify" "application/x-www-form-urlencoded" postData
putStrLn $ show request
-- Make the request
(Right response) <- H.simpleHTTP request
-- Print status code
putStrLn $ show $ H.rspCode response
-- Print response
putSrLn $ show $ H.rspBody response
UPDATED:
Use the following to help you get a JSON value:
import qualified Data.ByteString.Lazy as LC
import qualified Data.ByteString.Char8 as C
import qualified Data.Aeson as DA
responseBody <- return $ H.rspBody response
responseJSON <- return (DA.decode (LC.fromChunks [C.pack responseBody]) :: Maybe DA.Value)
You'll have to make a request object to make a request. There are quite a few helpers. I meant the post request as the most generic case:
http://hackage.haskell.org/package/HTTP-4000.0.5/docs/Network-HTTP.html
Since you are in the IO monad, all the <- are going to assume that you are dealing with IO operations. When you write
do
Right jsonValue <- maybeJson
Object jsonObject <- jsonValue
you are saying that jsonValue must be an IO action just like maybeJson. But this is not the case! jsonValue is but a regular Either value. The silution here would ge to use a do-block let instead of a <-:
do
Right jsonValue <- maybeJson
let Object jsonObject = jsonValue
However, its important to note that in both versions of your code you are using an irrecoverable error to abort your program if the JSON parsing fails. If you want to be able to collect errors, the basic idea would be to convert your values to Either (and then use the monad instance for Either to avoid having lots of nested case expressions)

Aeson parsing dynamic objects

I need to parse json API responses which don't have strict structure:
{
response: { /* any object here */ }
}
How is it possible to write parseResponse which will leave ability to parse (or choose a Parser for it) for later use?
My last try is below. I don't like it because it doesn't allow to choose response type as Aeson's decode does.
data APIResponse =
APIResponse { response :: Value } deriving (Show,Generic)
instance FromJSON APIResponse
parseResponse :: BC.ByteString -> Either String Value
parseResponse resp =
case eitherDecode . BLC.fromStrict $ resp of
Right x -> Right $ response x
Left msg -> Left msg
I like to think of Aeson as happening in two distinct steps, the parsing from ByteString -> Value and the mapping from FromJSON a => Value -> a. If you don't immediately know what the correct mapping is you can simply parse to a Value as you've done. Later at runtime when you decide the proper mapping you can do that later.
import qualified Data.HashMap.Strict as Hm
decodeResponse :: ByteString -> Either String Value
decodeResponse bs = do
val <- eitherDecode bs
case val of
Object o -> case Hm.lookup "response" o of
Nothing -> fail "Invalid structure, missing 'response' key"
Just resp -> return resp
_ -> fail "Invalid structure, expected an object"
To perform a mapping without a parse, to convert a Value to a FromJSON a => a, you use the parse/parseEither/parseMaybe family and run the parser generated by parseJSON
mapValue :: FromJSON a => Value -> Either String a
mapValue = parseString parseJSON

How do I keep non-string parts of JSON as strings using aeson?

I have a server that receives JSON that looks like:
{ "foo": "bar", "bono": "bobo",
"result": { "some": ["complex", "JSON", "structure",...
}
where all the stuff is for the Server except for "result", which is to be forwarded to the client (Worker --JSON--> Server --value of "result"--> Client). Therefore, when parsing this thing with aeson, I want to keep the value of "result" as a string (or Text or whatnot), just so I can forward it to the client without caring what's inside. Problem is that "result" can be anything (array, object, etc.).
So If I do
data RPCResult = RPCResult { foo :: Text, result :: Text }
the decode function of aeson is going to return Nothing, since "result" is not necessarily a JSON string...
How do I tell aeson to keep parts of the JSON object as-is and just give them to me so I can do what I want with them?
I seem to have somewhat of a solution by making the type of result be Data.Aeson.Value and then after doing a decode on the incoming JSON, I extract the result and run encode on it before forwarding it. I'm not sure if this is the best solution (because I'm not "keeping it as a string" as the question states, but decoding it then encoding it again...) but it works:
import Data.Aeson
import Data.Maybe
import Data.ByteString.Lazy (fromStrict, ByteString)
import Data.ByteString.UTF8 (fromString)
import Control.Applicative
data RPCResult = RPCResult { foo :: Text, result :: Value }
instance FromJSON RPCResult where
parseJSON (Object v) = RPCResult <$> v .: "foo" <*> v .: "result"
-- example just so you get the idea:
toRPCResult :: String -> RPCResult
toRPCResult = fromJust . decode . fromStrict . fromString
getResult :: String -> ByteString
getResult = encode . result . toRPCResult

Resources