packages feed

geo-resolver (empty) → 0.1.0.1

raw patch · 7 files changed

+587/−0 lines, 7 filesdep +HUnitdep +QuickCheckdep +aesonsetup-changed

Dependencies added: HUnit, QuickCheck, aeson, base, base64-bytestring, blaze-builder, bytestring, geo-resolver, http-conduit, http-types, test-framework, test-framework-hunit, test-framework-quickcheck2, text, unordered-containers

Files

+ LICENSE view
@@ -0,0 +1,19 @@+Copyright (c) 2015 Jan Greve, Markenwerk GmbH++Permission is hereby granted, free of charge, to any person obtaining a copy+of this software and associated documentation files (the "Software"), to deal+in the Software without restriction, including without limitation the rights+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell+copies of the Software, and to permit persons to whom the Software is+furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in+all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN+THE SOFTWARE.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ geo-resolver.cabal view
@@ -0,0 +1,47 @@+name:                geo-resolver+version:             0.1.0.1+synopsis:            Performs geo location lookups and parses the results +description:         Please see README.md+homepage:            https://github.com/markenwerk/haskell-geo-resolver/+license:             MIT+license-file:        LICENSE+author:              Jan Greve+maintainer:          jg@markenwerk.net+copyright:           2015 Jan Greve, Markenwerk GmbH+category:            Web+build-type:          Simple+-- extra-source-files:+cabal-version:       >=1.10++library+  hs-source-dirs:      src+  exposed-modules:     Network.Google.GeoResolver, Network.Google.GeoResolver.Parser, Network.Google.GeoResolver.Requester+  build-depends:       base >= 4.7 && < 5,+                       aeson >= 0.8.0.2 && < 1,+                       http-conduit >= 2.1.8 && < 2.2,+                       bytestring,+                       unordered-containers,+                       text,+                       http-types,+                       blaze-builder+  default-language:    Haskell2010++test-suite GeoResolver-test+  type:                exitcode-stdio-1.0+  hs-source-dirs:      test+  main-is:             Spec.hs+  build-depends:       base,+                       test-framework,+                       test-framework-hunit,+                       geo-resolver,+                       bytestring,+                       HUnit,+                       QuickCheck,+                       test-framework-quickcheck2,+                       base64-bytestring >= 1 && < 1.1+  ghc-options:         -threaded -rtsopts -with-rtsopts=-N+  default-language:    Haskell2010++source-repository head+  type:     git+  location: https://github.com/markenwerk/haskell-geo-resolver.git
+ src/Network/Google/GeoResolver.hs view
@@ -0,0 +1,106 @@+{-# LANGUAGE OverloadedStrings #-}++{-|+Module      : GeoResolver+Description : Simple way to access Google's geocoding API. Using http-conduit.+Copyright   : (c) 2015, Markenwerk, Jan Greve+License     : MIT+Maintainer  : jg@markenwerk.net+++This is supposed to offer an easy to use abstraction of the google geocoding+web service.++A minimum example of usage (with @OverloadedStrings@) is:++@+import GeoRequester+main = geoRequest "Lornsenstraße 43, Kiel" >>= putStrLn . show+@++While there are convenience functions for the most common use cases,+there are means to send arbitrary requests.++If you do so, please see "Geocoder.Parser" for ways to access the results.+As a hint, 'GoogleAnswer' is instance of 'Foldable' and 'Functor'.+++Please note that this Package is also meant to demonstrate basic Haskell features+and thus not always takes the most elegant way but the most educational way.++-}++module Network.Google.GeoResolver+    ( +      -- * Encoding+      geoEncode,+      geoEncodeLanguage,+      -- * Decoding+      geoDecode,+      geoDecodeLanguage,+      -- * Raw Requesting+      geoRequest,+      geoRaw+    ) where+import Network.Google.GeoResolver.Requester+import Network.Google.GeoResolver.Parser+import Control.Monad+import Data.Text (pack, append)+++-- | Encodes a given address into a coordinate.+geoEncode :: Maybe String -- ^ Optional API key to use if you intend to go over the courtesy limit+  -- Google imposes +  -> String -- ^ The address to be looked up.+  -> IO (Either String (Double, Double)) -- ^ Either an error message or the result.+geoEncode mk x =+   liftM (parseAnswer >=> getLocation) (requestEncode (fmap pack mk) (pack x))++-- | Encodes a given address into a coordinate.+geoEncodeLanguage :: Maybe String -- ^ Optional API key to use if you intend to go over the courtesy limit+  -- Google imposes +  -> String -- ^ The address to be looked up.+  -> String -- ^ Language to be used. cf. https://developers.google.com/maps/faq#languagesupport+  -> IO (Either String (Double, Double)) -- ^ Either an error message or the result.+geoEncodeLanguage Nothing addr lang =+    liftM (parseAnswer >=> getLocation)+        (requestRaw [("address", pack addr), ("language", pack lang)])++geoEncodeLanguage (Just k) addr lang =+    liftM (parseAnswer >=> getLocation)+        (requestRaw [("address", pack addr), ("language", pack lang), ("key", pack k)])++-- | Reverse geocoding of a given coordinate pair into an address.+geoDecode :: Maybe String -- ^ Optional API key to use if you intend to go over the courtesy limit+  -- Google imposes +  -> (Double, Double) -- ^ coordinate to be decoded+  -> IO (Either String String) -- ^ Either an error message (Left) or the result (Right).+geoDecode mk x =+    liftM (parseAnswer >=> getAddress)+        (requestDecode (fmap pack mk) x)++-- | Reverse geocoding of a given coordinate pair into an address.+geoDecodeLanguage :: Maybe String -- ^ Optional API key to use if you intend to go over the courtesy limit+  -- Google imposes +  -> (Double, Double) -- ^ coordinate to be decoded+  -> String -- ^ Language to be used. cf. https://developers.google.com/maps/faq#languagesupport+  -> IO (Either String String) -- ^ Either an error message (Left) or the result (Right).+geoDecodeLanguage Nothing (lat, lng) lang =+    liftM (parseAnswer >=> getAddress)+        (requestRaw+            [("latlng",pack (show lat) `append` pack (',' : show lng)), ("language", pack lang)])+geoDecodeLanguage (Just k) (lat, lng) lang =+    liftM (parseAnswer >=> getAddress)+        (requestRaw+            [("latlng",pack (show lat) `append` pack (',' : show lng)), ("language", pack lang), ("key", pack k)])++-- | Sends a request based on a 'GoogleRequest'.+geoRequest :: GoogleRequest -> IO (Either String (GoogleAnswer GoogleResult))+geoRequest r = liftM parseAnswer (requestRequest r)+++-- | Sending a raw request to the api, if you want more control than the above methods offer.+-- Uses a pair of key-value pairs to generate the actual query.+-- See "GeoResolver.Parser" for a helping hand using the resulting 'GoogleAnswer'.+geoRaw :: [(String, String)] -> IO (Either String (GoogleAnswer GoogleResult))+geoRaw xs = liftM parseAnswer (requestRaw (map (\(k,v) -> (pack k, pack v)) xs))
+ src/Network/Google/GeoResolver/Parser.hs view
@@ -0,0 +1,213 @@+{-# LANGUAGE OverloadedStrings, DeriveGeneric #-}+{-|+Module      : GeoResolver.Parser+Description : Parsing helper definitions for Googles geocoding API.+Copyright   : (c) 2015, Markenwerk, Jan Greve+License     : MIT+Maintainer  : jg@markenwerk.net+-}+module Network.Google.GeoResolver.Parser (+    -- * Data type definition for parsed types+    Status(..),+    GoogleAnswer(..),+    GoogleResult(..),+    Component(..),+    Geometry(..),+    GoogleBoundingBox(..),+    Location(..),+    -- * parsing+    parseAnswer,+    -- * convenience functions+    getLocation,+    getAddress,+    getProperty,+    -- * Typeclasses+    GoogleArgumentListShow(..)+    ) where ++import GHC.Generics+import Data.Aeson+import Data.HashMap.Strict+import Data.Text+import Data.Maybe (fromMaybe)+import Control.Arrow+import qualified Data.ByteString.Lazy.Char8 as LBS+++-- | A class to format instances the way google expects them.+class GoogleArgumentListShow a where+  argListShow :: a -> String+++-- | Represents the status of the operation returned by google. Constructors represent possible 'String' values+-- according to googles documentation.+data Status = OK | ZERO_RESULTS | OVER_QUERY_LIMIT | REQUEST_DENIED | INVALID_REQUEST | UNKNOWN_ERROR+    deriving (Eq, Show, Generic)+instance FromJSON Status++-- | Represents the answer google returned.+data GoogleAnswer r = GoogleAnswer {+    -- | The 'Status' returned by google.+    status :: Status,+    -- | An optional error message. +    errorMessage :: Maybe String,+    -- | Optional list of actual 'GoogleResult' values. +    results :: Maybe [r]+    } deriving (Show, Eq)+instance (FromJSON a) => FromJSON (GoogleAnswer a) where+    parseJSON (Object v) = GoogleAnswer <$>+                           v .: "status" <*>+                           v .:? "error_message" <*>+                           v .:? "results"+    parseJSON _          = mempty++instance Functor GoogleAnswer where+    fmap f (GoogleAnswer s e Nothing) = (GoogleAnswer s e Nothing)+    fmap f (GoogleAnswer s e (Just xs)) = (GoogleAnswer s e (Just $ fmap f xs))++instance Foldable GoogleAnswer where+    foldMap _ (GoogleAnswer _ _ Nothing) = mempty+    foldMap f (GoogleAnswer _ _ (Just [r])) = f r+    foldMap f (GoogleAnswer _ _ (Just rs)) = foldMap f rs+++-- | A single Result from the list of results from google.+data GoogleResult = GoogleResult {+    -- | List of 'Component' values for this result+    addressComponents :: [Component],+    -- | The formatted address google returned+    formattedAddress :: String,+    -- | The 'Geometry' value for this result+    geometry :: Geometry,+    -- | The google places ID for this result+    placeId :: String,+    -- | Some list of strings. Goole says:+    -- +    -- The types[] array indicates the type of the returned result. +    -- This array contains a set of zero or more tags identifying the type of feature returned in the result.+    -- For example, a geocode of "Chicago" returns "locality" which indicates that "Chicago" is a city, +    -- and also returns "political" which indicates it is a political entity.+    types :: [String],+    -- | If present, hinting that this result only partially matches the requested entity.+    partialMatch :: Maybe Bool+    } deriving (Show, Generic)+instance FromJSON GoogleResult where+    parseJSON (Object v) = GoogleResult <$>+                           v .: "address_components" <*>+                           v .: "formatted_address" <*>+                           v .: "geometry" <*>+                           v .: "place_id" <*>+                           v .: "types" <*>+                           v .:? "partial_match"+    parseJSON _          = mempty++-- | A part of the address in a 'GoogleResult'+data Component = Component {+    -- | A long name for the component+    longName :: String,+    -- | A short name for the component+    shortName :: String,+    -- | indicating the type of the address component.+    cTypes :: [String]+    } deriving (Show, Eq)+instance FromJSON Component where+    parseJSON (Object v) = Component <$>+                           v .: "long_name" <*>+                           v .: "short_name" <*>+                           v .: "types"+    parseJSON _          = mempty+++-- | Holds geometry information about a 'GoogleResult' +data Geometry = Geometry {+    -- | The result's location+    location :: Location,+    -- | The kind of location. As of 2015/10/07, the+    -- following values are to be expected. For future compatibility,+    -- no Enum type is introduced to map this.+    -- +    -- * @ROOFTOP@ indicates that the returned result is a precise geocode for +    -- which we have location information accurate down to street address precision.+    --+    -- * @RANGE_INTERPOLATED@ indicates that the returned result reflects +    -- an approximation (usually on a road) interpolated between two precise points +    -- (such as intersections). Interpolated results are generally returned +    -- when rooftop geocodes are unavailable for a street address.+    --+    -- * @GEOMETRIC_CENTER@ indicates that the returned result is the geometric +    -- center of a result such as a polyline (for example, a street) or +    -- polygon (region).+    --+    -- * @APPROXIMATE@ indicates that the returned result is approximate.+    locationType :: String,+    -- | contains the recommended viewport for displaying the returned result. +    -- Generally the viewport+    -- is used to frame a result when displaying it to a user.+    viewport :: GoogleBoundingBox,+    -- | If viewport is not applicable, bounds contain a more sensible bounding box.+    bounds :: Maybe GoogleBoundingBox+    } deriving (Show, Eq)+instance FromJSON Geometry where+    parseJSON (Object v) = Geometry <$>+                           v .: "location" <*>+                           v .: "location_type" <*>+                           v .: "viewport" <*>+                           v .:? "bounds"+    parseJSON _          = mempty++-- | A Bounding box for a location.+data GoogleBoundingBox = GoogleBoundingBox {+    -- | The north east location of the bounding box+    northeast :: Location,+    -- | The south west location of the bounding box+    southwest :: Location+} deriving (Show, Eq, Generic)+instance FromJSON GoogleBoundingBox+instance GoogleArgumentListShow GoogleBoundingBox where+    argListShow (GoogleBoundingBox ne sw) = argListShow ne ++ '|' : argListShow sw+++-- | Abstraction of a geo location +data Location = Location {+    -- | Latitude of the location+    latitude :: Double,+    -- | Longitude of the location+    longitude :: Double+    } deriving (Show, Eq)+instance GoogleArgumentListShow Location where+    argListShow (Location lat long) = show lat ++ ',': show long++instance FromJSON Location where+    parseJSON (Object o) = Location <$>+        o .: "lat" <*>+        o .: "lng"+    parseJSON _ = mempty++-- | Takes a 'GoogleAnswer' and applies the function to the first 'GoogleResult'.+-- Returns a 'Left' with an error description if anything unexpected happens.+--+-- For example, 'getLocation' uses this with +-- @+--  ((latitude &&& longitude) . location . geometry)+-- @+getProperty :: GoogleAnswer r -- ^ The answer to process.+    -> (r -> a) -- ^ The function to be applied to the first (if any) result.+    -> Either String a -- ^ Error or result of the function application+getProperty a f = case status a of+    OK -> fromMaybe (Left "No results.") (results a >>= (\res -> case res of+        (x:_) -> (Just . Right . f) x+        _ -> Just $ Left "Empty resultset"))+    otherwise -> Left (show otherwise ++ show (errorMessage a))+++-- | Gets the location from a 'GoogleAnswer', or returns an error.+getLocation :: GoogleAnswer GoogleResult -> Either String (Double, Double)+getLocation a = a `getProperty` ((latitude &&& longitude) . location . geometry)++-- | Gets the formatted address from a GoogleAnswer (or an error)+getAddress :: GoogleAnswer GoogleResult -> Either String String+getAddress a = a `getProperty` formattedAddress++-- | Parses a Lazy ByteString into a 'GoogleAnswer' or returns an error describing the problem.+parseAnswer :: LBS.ByteString -> Either String (GoogleAnswer GoogleResult)+parseAnswer = eitherDecode
+ src/Network/Google/GeoResolver/Requester.hs view
@@ -0,0 +1,129 @@+{-# LANGUAGE OverloadedStrings #-}+{-|+Module      : GeoResolver.Requester+Description : Request helper definitions for Googles geocoding API. Using http-conduit.+Copyright   : (c) 2015, Markenwerk, Jan Greve+License     : MIT+Maintainer  : jg@markenwerk.net+-}+module Network.Google.GeoResolver.Requester (+    -- * Data Types+    GoogleRequest(..),+    GoogleComponents(..),+    GoogleLocationTypes(..),+    GoogleResultTypes(..),+    -- * Request methods+    requestEncode,+    requestDecode,+    requestRaw,+    requestRequest++    ) where++import Network.HTTP.Types+import Network.HTTP.Conduit+import Blaze.ByteString.Builder+import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString.Lazy.Char8 as LBSC+import Data.Text (Text, append, pack)+import Network.Google.GeoResolver.Parser (GoogleBoundingBox(..), Location(..), GoogleArgumentListShow(..))+import Data.Maybe+import Control.Arrow (second)+import Data.String (IsString(..))++-- | A Type abstracting possible API request argument combinations.+--+-- For convenience, the 'IsString' instance generates a encoding request and assumes the String+-- is the address.+data GoogleRequest = +  EncodingRequest {+    encodeParameter :: Either String GoogleComponents,+    encodeBounds :: Maybe GoogleBoundingBox,+    encodeLanguage :: Maybe String,+    encodeRegion :: Maybe String,+    encodeKey :: Maybe String+  }+  | DecodingRequest {+    decodeParameter :: Either Location String,+    decodeKey :: Maybe String,+    decodeLanguage :: Maybe String,+    decodeResultType :: Maybe GoogleResultTypes,+    decodeLocationType :: Maybe GoogleLocationTypes+}++instance IsString GoogleRequest where+  fromString s = EncodingRequest (Left s) Nothing Nothing Nothing Nothing++instance GoogleArgumentList GoogleRequest where+  argShow (EncodingRequest (Left addr) b l r k) = map (second fromJust) $ filter (isJust . snd) $+    zip+      ["address", "bounds", "language", "region", "key"]+      (fmap (fmap pack) [Just addr, fmap argListShow b, l, r, k])+  argShow (EncodingRequest (Right c) b l r k) = map (second fromJust) $ filter (isJust . snd) $+    zip+      ["components", "bounds", "language", "region", "key"]+      (fmap (fmap pack) [Just (argListShow c), fmap argListShow b, l, r, k])+  argShow (DecodingRequest (Left loc) k l rt lt) = map (second fromJust) $ filter (isJust . snd) $+    zip+      ["latlng", "key", "language", "result_type", "location_type"]+      (fmap (fmap pack) [Just (argListShow loc), k, l, fmap argListShow rt, fmap argListShow lt])+  argShow (DecodingRequest (Right pid) k l rt lt) = map (second fromJust) $ filter (isJust . snd) $+    zip+      ["place_id", "key", "language", "result_type", "location_type"]+      (fmap (fmap pack) [Just pid, k, l, fmap argListShow rt, fmap argListShow lt])++class GoogleArgumentList a where+  argShow :: a -> [(Text, Text)]+++-- | Abstraction for google's components+data GoogleComponents = Components [String]+instance GoogleArgumentListShow GoogleComponents where+  argListShow (Components []) = ""+  argListShow (Components (x : xs)) = x ++ concatMap ('|' :) xs++-- | Abstraction for google's result types+data GoogleResultTypes = ResultTypes [String]+instance GoogleArgumentListShow GoogleResultTypes where+  argListShow (ResultTypes []) = ""+  argListShow (ResultTypes (x : xs)) = x ++ concatMap ('|' :) xs+-- | Abstraction for google's location types+data GoogleLocationTypes = LocationTypes [String]+instance GoogleArgumentListShow GoogleLocationTypes where+  argListShow (LocationTypes []) = ""+  argListShow (LocationTypes (x : xs)) = x ++ concatMap ('|' :) xs+++baseURL :: Builder+baseURL = "https://maps.googleapis.com/maps/api/geocode/json"++uriFromQueryPairs :: [(Text, Text)] -> LBS.ByteString+uriFromQueryPairs ps = toLazyByteString $ baseURL `mappend` query+  where query = renderQueryBuilder True $ queryTextToQuery (map (second Just) ps)++-- | Constructs the URI to be used for the web service+-- invocation from the input. Sends a request and+-- returns the Lazy ByteString from IO.+requestRaw :: [(Text, Text)] -> IO LBS.ByteString+requestRaw = simpleHttp . LBSC.unpack . uriFromQueryPairs++uriFromAddress :: Maybe Text -> Text -> LBS.ByteString+uriFromAddress Nothing x = uriFromQueryPairs [("address",x)]+uriFromAddress (Just k) x = uriFromQueryPairs [("address",x), ("key", k)]+++uriFromLocation :: Maybe Text -> (Double, Double) -> LBS.ByteString+uriFromLocation Nothing (lat, lng) = uriFromQueryPairs [("latlng", pack (show lat) `append` (pack $ ',':show lng))]+uriFromLocation (Just k) (lat, lng) = uriFromQueryPairs [("latlng", pack (show lat) `append` (pack $ ',':show lng)), ("key", k)]++-- | Convenience function to request a given address.+requestEncode :: Maybe Text -> Text -> IO LBS.ByteString+requestEncode mk = simpleHttp . LBSC.unpack . uriFromAddress mk++-- | Convenience function to request a given location.+requestDecode :: Maybe Text  -> (Double, Double) -> IO LBS.ByteString+requestDecode mk = simpleHttp . LBSC.unpack . uriFromLocation mk++-- | Sends a request based on a 'GoogleRequest'.+requestRequest :: GoogleRequest -> IO LBS.ByteString+requestRequest = simpleHttp . LBSC.unpack . uriFromQueryPairs . argShow
+ test/Spec.hs view
@@ -0,0 +1,71 @@+import Network.Google.GeoResolver+import Network.Google.GeoResolver.Parser+import Test.Framework (defaultMain, testGroup)+import Test.HUnit+import Test.HUnit.Base+import Test.Framework.Providers.HUnit+import qualified Data.ByteString.Lazy.Char8 as LBS+import Data.ByteString.Base64.Lazy (decodeLenient)+import Test.Framework.Providers.QuickCheck2++main :: IO ()+main = defaultMain tests+++tests = [++        testGroup "Resolving (Encode)" +            [testCase "Resolving Home" testResolvingHere,+            testCase "Resolving Empty String" testResolvNowhere],+        testGroup "Reverse resolving (Decoding)"+            [+            testCase "Placeholder" ( True @=? True),+            testCase "Placeholder 2" ( True @=? True),+            testCase "Placeholder 3" ( True @=? True)++++            ],++        testGroup "Parsing JSON"+            [testCase "invalid JSON" testInvalid,+            testCase "valid, but not sufficient" testValidMissing,+            testCase "Google Example" testExample,+            testCase "Google Example 2" testExample2+        ]+++++    ]++testInvalid = True @=? parserResult `hasLeft` (const True)+    where parserResult = parseAnswer (LBS.pack "[}")++testValidMissing = True @=? parserResult `hasLeft` (const True)+    where parserResult = parseAnswer (LBS.pack "{\"not status\":\"OK\"}")++testExample = True @=? parserResult `hasRight` (const True)+    where parserResult = parseAnswer (decodeLenient $ LBS.pack "ew0KICAgInJlc3VsdHMiIDogWw0KICAgICAgew0KICAgICAgICAgImFkZHJlc3NfY29tcG9uZW50cyIgOiBbDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAibG9uZ19uYW1lIiA6ICJTYW50YSBDcnV6IGRlIFRlbmVyaWZlIiwNCiAgICAgICAgICAgICAgICJzaG9ydF9uYW1lIiA6ICJTYW50YSBDcnV6IGRlIFRlbmVyaWZlIiwNCiAgICAgICAgICAgICAgICJ0eXBlcyIgOiBbICJsb2NhbGl0eSIsICJwb2xpdGljYWwiIF0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAibG9uZ19uYW1lIiA6ICJTYW50YSBDcnV6IGRlIFRlbmVyaWZlIiwNCiAgICAgICAgICAgICAgICJzaG9ydF9uYW1lIiA6ICJTYW50YSBDcnV6IGRlIFRlbmVyaWZlIiwNCiAgICAgICAgICAgICAgICJ0eXBlcyIgOiBbICJhZG1pbmlzdHJhdGl2ZV9hcmVhX2xldmVsXzQiLCAicG9saXRpY2FsIiBdDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgImxvbmdfbmFtZSIgOiAiQW5hZ2EiLA0KICAgICAgICAgICAgICAgInNob3J0X25hbWUiIDogIkFuYWdhIiwNCiAgICAgICAgICAgICAgICJ0eXBlcyIgOiBbICJhZG1pbmlzdHJhdGl2ZV9hcmVhX2xldmVsXzMiLCAicG9saXRpY2FsIiBdDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgImxvbmdfbmFtZSIgOiAiU2FudGEgQ3J1eiBkZSBUZW5lcmlmZSIsDQogICAgICAgICAgICAgICAic2hvcnRfbmFtZSIgOiAiVEYiLA0KICAgICAgICAgICAgICAgInR5cGVzIiA6IFsgImFkbWluaXN0cmF0aXZlX2FyZWFfbGV2ZWxfMiIsICJwb2xpdGljYWwiIF0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAibG9uZ19uYW1lIiA6ICJDYW5hcmlhcyIsDQogICAgICAgICAgICAgICAic2hvcnRfbmFtZSIgOiAiQ04iLA0KICAgICAgICAgICAgICAgInR5cGVzIiA6IFsgImFkbWluaXN0cmF0aXZlX2FyZWFfbGV2ZWxfMSIsICJwb2xpdGljYWwiIF0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAibG9uZ19uYW1lIiA6ICJTcGFpbiIsDQogICAgICAgICAgICAgICAic2hvcnRfbmFtZSIgOiAiRVMiLA0KICAgICAgICAgICAgICAgInR5cGVzIiA6IFsgImNvdW50cnkiLCAicG9saXRpY2FsIiBdDQogICAgICAgICAgICB9DQogICAgICAgICBdLA0KICAgICAgICAgImZvcm1hdHRlZF9hZGRyZXNzIiA6ICJTYW50YSBDcnV6IGRlIFRlbmVyaWZlLCBTYW50YSBDcnV6IGRlIFRlbmVyaWZlLCBTcGFpbiIsDQogICAgICAgICAiZ2VvbWV0cnkiIDogew0KICAgICAgICAgICAgImJvdW5kcyIgOiB7DQogICAgICAgICAgICAgICAibm9ydGhlYXN0IiA6IHsNCiAgICAgICAgICAgICAgICAgICJsYXQiIDogMjguNDg3NjE2LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAtMTYuMjM1NjY0Ng0KICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICJzb3V0aHdlc3QiIDogew0KICAgICAgICAgICAgICAgICAgImxhdCIgOiAyOC40MjgwMjQ4LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAtMTYuMzM3MDA0NQ0KICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICJsb2NhdGlvbiIgOiB7DQogICAgICAgICAgICAgICAibGF0IiA6IDI4LjQ2MzYyOTYsDQogICAgICAgICAgICAgICAibG5nIiA6IC0xNi4yNTE4NDY3DQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgImxvY2F0aW9uX3R5cGUiIDogIkFQUFJPWElNQVRFIiwNCiAgICAgICAgICAgICJ2aWV3cG9ydCIgOiB7DQogICAgICAgICAgICAgICAibm9ydGhlYXN0IiA6IHsNCiAgICAgICAgICAgICAgICAgICJsYXQiIDogMjguNDg3NjE2LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAtMTYuMjM1NjY0Ng0KICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICJzb3V0aHdlc3QiIDogew0KICAgICAgICAgICAgICAgICAgImxhdCIgOiAyOC40MjgwMjQ4LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAtMTYuMzM3MDA0NQ0KICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgfQ0KICAgICAgICAgfSwNCiAgICAgICAgICJwbGFjZV9pZCIgOiAiQ2hJSmNVRWx6T3pNUVF3Ukx1VjMwbk1VRVVNIiwNCiAgICAgICAgICJ0eXBlcyIgOiBbICJsb2NhbGl0eSIsICJwb2xpdGljYWwiIF0NCiAgICAgIH0NCiAgIF0sDQogICAic3RhdHVzIiA6ICJPSyINCn0NCg==")++testExample2 = True @=? parserResult `hasRight` (const True)+    where parserResult = parseAnswer (decodeLenient $ LBS.pack "ew0KICAgInJlc3VsdHMiIDogWw0KICAgICAgew0KICAgICAgICAgImFkZHJlc3NfY29tcG9uZW50cyIgOiBbDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAibG9uZ19uYW1lIiA6ICJBbm5lZ2F0YW4iLA0KICAgICAgICAgICAgICAgInNob3J0X25hbWUiIDogIkFubmVnYXRhbiIsDQogICAgICAgICAgICAgICAidHlwZXMiIDogWyAicm91dGUiIF0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAibG9uZ19uYW1lIiA6ICJIZWxzaW5nZm9ycyIsDQogICAgICAgICAgICAgICAic2hvcnRfbmFtZSIgOiAiSGVsc2luZ2ZvcnMiLA0KICAgICAgICAgICAgICAgInR5cGVzIiA6IFsgImFkbWluaXN0cmF0aXZlX2FyZWFfbGV2ZWxfMyIsICJwb2xpdGljYWwiIF0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICB7DQogICAgICAgICAgICAgICAibG9uZ19uYW1lIiA6ICJGaW5sYW5kIiwNCiAgICAgICAgICAgICAgICJzaG9ydF9uYW1lIiA6ICJGSSIsDQogICAgICAgICAgICAgICAidHlwZXMiIDogWyAiY291bnRyeSIsICJwb2xpdGljYWwiIF0NCiAgICAgICAgICAgIH0NCiAgICAgICAgIF0sDQogICAgICAgICAiZm9ybWF0dGVkX2FkZHJlc3MiIDogIkFubmVnYXRhbiwgSGVsc2luZ2ZvcnMsIEZpbmxhbmQiLA0KICAgICAgICAgImdlb21ldHJ5IiA6IHsNCiAgICAgICAgICAgICJib3VuZHMiIDogew0KICAgICAgICAgICAgICAgIm5vcnRoZWFzdCIgOiB7DQogICAgICAgICAgICAgICAgICAibGF0IiA6IDYwLjE2ODk5NywNCiAgICAgICAgICAgICAgICAgICJsbmciIDogMjQuOTQyNzk1OQ0KICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICJzb3V0aHdlc3QiIDogew0KICAgICAgICAgICAgICAgICAgImxhdCIgOiA2MC4xNjI2NjI3LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAyNC45MzQNCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICAibG9jYXRpb24iIDogew0KICAgICAgICAgICAgICAgImxhdCIgOiA2MC4xNjU3ODA4LA0KICAgICAgICAgICAgICAgImxuZyIgOiAyNC45Mzg0NTENCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICAibG9jYXRpb25fdHlwZSIgOiAiR0VPTUVUUklDX0NFTlRFUiIsDQogICAgICAgICAgICAidmlld3BvcnQiIDogew0KICAgICAgICAgICAgICAgIm5vcnRoZWFzdCIgOiB7DQogICAgICAgICAgICAgICAgICAibGF0IiA6IDYwLjE2ODk5NywNCiAgICAgICAgICAgICAgICAgICJsbmciIDogMjQuOTQyNzk1OQ0KICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICJzb3V0aHdlc3QiIDogew0KICAgICAgICAgICAgICAgICAgImxhdCIgOiA2MC4xNjI2NjI3LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAyNC45MzQNCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0NCiAgICAgICAgIH0sDQogICAgICAgICAicGxhY2VfaWQiIDogIkNoSUpBUlc3QzhzTGtrWVJnbDRqZTQtUlBVTSIsDQogICAgICAgICAidHlwZXMiIDogWyAicm91dGUiIF0NCiAgICAgIH0sDQogICAgICB7DQogICAgICAgICAiYWRkcmVzc19jb21wb25lbnRzIiA6IFsNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICJsb25nX25hbWUiIDogIkFubmV2w6RnZW4iLA0KICAgICAgICAgICAgICAgInNob3J0X25hbWUiIDogIkFubmV2w6RnZW4iLA0KICAgICAgICAgICAgICAgInR5cGVzIiA6IFsgInJvdXRlIiBdDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgImxvbmdfbmFtZSIgOiAiVmFuZGEiLA0KICAgICAgICAgICAgICAgInNob3J0X25hbWUiIDogIlZhbmRhIiwNCiAgICAgICAgICAgICAgICJ0eXBlcyIgOiBbICJhZG1pbmlzdHJhdGl2ZV9hcmVhX2xldmVsXzMiLCAicG9saXRpY2FsIiBdDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgImxvbmdfbmFtZSIgOiAiRmlubGFuZCIsDQogICAgICAgICAgICAgICAic2hvcnRfbmFtZSIgOiAiRkkiLA0KICAgICAgICAgICAgICAgInR5cGVzIiA6IFsgImNvdW50cnkiLCAicG9saXRpY2FsIiBdDQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgImxvbmdfbmFtZSIgOiAiMDE0MjAiLA0KICAgICAgICAgICAgICAgInNob3J0X25hbWUiIDogIjAxNDIwIiwNCiAgICAgICAgICAgICAgICJ0eXBlcyIgOiBbICJwb3N0YWxfY29kZSIgXQ0KICAgICAgICAgICAgfQ0KICAgICAgICAgXSwNCiAgICAgICAgICJmb3JtYXR0ZWRfYWRkcmVzcyIgOiAiQW5uZXbDpGdlbiwgMDE0MjAgVmFuZGEsIEZpbmxhbmQiLA0KICAgICAgICAgImdlb21ldHJ5IiA6IHsNCiAgICAgICAgICAgICJib3VuZHMiIDogew0KICAgICAgICAgICAgICAgIm5vcnRoZWFzdCIgOiB7DQogICAgICAgICAgICAgICAgICAibGF0IiA6IDYwLjMyODI3MzgsDQogICAgICAgICAgICAgICAgICAibG5nIiA6IDI1LjExNjIxNjMNCiAgICAgICAgICAgICAgIH0sDQogICAgICAgICAgICAgICAic291dGh3ZXN0IiA6IHsNCiAgICAgICAgICAgICAgICAgICJsYXQiIDogNjAuMzI1NjQwMDk5OTk5OTksDQogICAgICAgICAgICAgICAgICAibG5nIiA6IDI1LjEwNzY0NzQNCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICAibG9jYXRpb24iIDogew0KICAgICAgICAgICAgICAgImxhdCIgOiA2MC4zMjcxMDY5LA0KICAgICAgICAgICAgICAgImxuZyIgOiAyNS4xMTE4MDQ2DQogICAgICAgICAgICB9LA0KICAgICAgICAgICAgImxvY2F0aW9uX3R5cGUiIDogIkdFT01FVFJJQ19DRU5URVIiLA0KICAgICAgICAgICAgInZpZXdwb3J0IiA6IHsNCiAgICAgICAgICAgICAgICJub3J0aGVhc3QiIDogew0KICAgICAgICAgICAgICAgICAgImxhdCIgOiA2MC4zMjgzMDU5MzAyOTE1LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAyNS4xMTYyMTYzDQogICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgInNvdXRod2VzdCIgOiB7DQogICAgICAgICAgICAgICAgICAibGF0IiA6IDYwLjMyNTYwNzk2OTcwODQ5LA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAyNS4xMDc2NDc0DQogICAgICAgICAgICAgICB9DQogICAgICAgICAgICB9DQogICAgICAgICB9LA0KICAgICAgICAgInBhcnRpYWxfbWF0Y2giIDogdHJ1ZSwNCiAgICAgICAgICJwbGFjZV9pZCIgOiAiQ2hJSjNVSkNOdDRHa2tZUjgtX2E4RGgyNWtBIiwNCiAgICAgICAgICJ0eXBlcyIgOiBbICJyb3V0ZSIgXQ0KICAgICAgfSwNCiAgICAgIHsNCiAgICAgICAgICJhZGRyZXNzX2NvbXBvbmVudHMiIDogWw0KICAgICAgICAgICAgew0KICAgICAgICAgICAgICAgImxvbmdfbmFtZSIgOiAiQW5uZXBsYXRzZW4iLA0KICAgICAgICAgICAgICAgInNob3J0X25hbWUiIDogIkFubmVwbGF0c2VuIiwNCiAgICAgICAgICAgICAgICJ0eXBlcyIgOiBbICJyb3V0ZSIgXQ0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICJsb25nX25hbWUiIDogIkhlbHNpbmdmb3JzIiwNCiAgICAgICAgICAgICAgICJzaG9ydF9uYW1lIiA6ICJIZWxzaW5nZm9ycyIsDQogICAgICAgICAgICAgICAidHlwZXMiIDogWyAiYWRtaW5pc3RyYXRpdmVfYXJlYV9sZXZlbF8zIiwgInBvbGl0aWNhbCIgXQ0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICJsb25nX25hbWUiIDogIkZpbmxhbmQiLA0KICAgICAgICAgICAgICAgInNob3J0X25hbWUiIDogIkZJIiwNCiAgICAgICAgICAgICAgICJ0eXBlcyIgOiBbICJjb3VudHJ5IiwgInBvbGl0aWNhbCIgXQ0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgIHsNCiAgICAgICAgICAgICAgICJsb25nX25hbWUiIDogIjAwMTAwIiwNCiAgICAgICAgICAgICAgICJzaG9ydF9uYW1lIiA6ICIwMDEwMCIsDQogICAgICAgICAgICAgICAidHlwZXMiIDogWyAicG9zdGFsX2NvZGUiIF0NCiAgICAgICAgICAgIH0NCiAgICAgICAgIF0sDQogICAgICAgICAiZm9ybWF0dGVkX2FkZHJlc3MiIDogIkFubmVwbGF0c2VuLCAwMDEwMCBIZWxzaW5nZm9ycywgRmlubGFuZCIsDQogICAgICAgICAiZ2VvbWV0cnkiIDogew0KICAgICAgICAgICAgImJvdW5kcyIgOiB7DQogICAgICAgICAgICAgICAibm9ydGhlYXN0IiA6IHsNCiAgICAgICAgICAgICAgICAgICJsYXQiIDogNjAuMTY5NTY2NCwNCiAgICAgICAgICAgICAgICAgICJsbmciIDogMjQuOTM1NzEyNQ0KICAgICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICAgICJzb3V0aHdlc3QiIDogew0KICAgICAgICAgICAgICAgICAgImxhdCIgOiA2MC4xNjg5OTcsDQogICAgICAgICAgICAgICAgICAibG5nIiA6IDI0LjkzNA0KICAgICAgICAgICAgICAgfQ0KICAgICAgICAgICAgfSwNCiAgICAgICAgICAgICJsb2NhdGlvbiIgOiB7DQogICAgICAgICAgICAgICAibGF0IiA6IDYwLjE2OTI3NDEsDQogICAgICAgICAgICAgICAibG5nIiA6IDI0LjkzNDgwMTYNCiAgICAgICAgICAgIH0sDQogICAgICAgICAgICAibG9jYXRpb25fdHlwZSIgOiAiR0VPTUVUUklDX0NFTlRFUiIsDQogICAgICAgICAgICAidmlld3BvcnQiIDogew0KICAgICAgICAgICAgICAgIm5vcnRoZWFzdCIgOiB7DQogICAgICAgICAgICAgICAgICAibGF0IiA6IDYwLjE3MDYzMDY4MDI5MTUxLA0KICAgICAgICAgICAgICAgICAgImxuZyIgOiAyNC45MzYyMDUyMzAyOTE1DQogICAgICAgICAgICAgICB9LA0KICAgICAgICAgICAgICAgInNvdXRod2VzdCIgOiB7DQogICAgICAgICAgICAgICAgICAibGF0IiA6IDYwLjE2NzkzMjcxOTcwODUsDQogICAgICAgICAgICAgICAgICAibG5nIiA6IDI0LjkzMzUwNzI2OTcwODUNCiAgICAgICAgICAgICAgIH0NCiAgICAgICAgICAgIH0NCiAgICAgICAgIH0sDQogICAgICAgICAicGFydGlhbF9tYXRjaCIgOiB0cnVlLA0KICAgICAgICAgInBsYWNlX2lkIiA6ICJDaElKZWFoTXFzd0xra1lSMnZRZkcxbkhJM00iLA0KICAgICAgICAgInR5cGVzIiA6IFsgInJvdXRlIiBdDQogICAgICB9DQogICBdLA0KICAgInN0YXR1cyIgOiAiT0siDQp9DQo=")++testResolvingHere :: Assertion+testResolvingHere = do+    result <- geoEncode Nothing "Lornsenstraße 43\nKiel"+    True @=? result `hasRight` (\(lat, long) -> and [lat < 55, lat > 54, long < 11, long > 10])++testResolvNowhere :: Assertion+testResolvNowhere = do+    result <- geoEncode Nothing ""+    False @=? result `hasLeft` (\x -> x == "INVALID_REQUEST")+++hasRight :: Either a b -> (b -> Bool) -> Bool+(Left _) `hasRight` _ = False+(Right x) `hasRight`  f = f x++hasLeft :: Either a b -> (a -> Bool) -> Bool+(Right _) `hasLeft` _ = False+(Left x) `hasLeft`  f = f x