packages feed

factual-api 0.2.0 → 0.3.0

raw patch · 23 files changed

+215/−86 lines, 23 filesdep +HTTPdep +bytestringdep +containers

Dependencies added: HTTP, bytestring, containers

Files

Data/Factual/Query/CrosswalkQuery.hs view
@@ -7,6 +7,7 @@  import Data.Factual.Query import Data.Factual.Utils+import Network.HTTP.Base (urlEncode)  -- | A crosswalk query can be formed by specifying a factual id and --   (optionally) a list of namespaces to only include when attempting to find@@ -32,17 +33,17 @@ -- The following helper functions are used to generate the separate parts of -- the query path. factualIdString :: Maybe String -> String-factualIdString (Just id) = "factual_id=" ++ id+factualIdString (Just id) = "factual_id=" ++ urlEncode id factualIdString Nothing   = ""  namespaceString :: Maybe String -> String-namespaceString (Just namespace) = "namespace=" ++ namespace+namespaceString (Just namespace) = "namespace=" ++ urlEncode namespace namespaceString Nothing          = ""  namespaceIdString :: Maybe String -> String-namespaceIdString (Just id) = "namespace_id=" ++ id+namespaceIdString (Just id) = "namespace_id=" ++ urlEncode id namespaceIdString Nothing   = ""  onlyString :: [String] -> String onlyString []   = ""-onlyString strs = "only=" ++ (join "," strs)+onlyString strs = "only=" ++ urlEncode (join "," strs)
Data/Factual/Query/FacetsQuery.hs view
@@ -16,6 +16,7 @@ import Data.Factual.Shared.Filter import Data.Factual.Shared.Geo import Data.Factual.Utils+import Network.HTTP.Base (urlEncode)  -- | The FacetsQuery type is used to construct facets queries. A table and search --   should be specified, but the rest of the query options are essentially@@ -45,5 +46,5 @@  -- Helper functions minCountString :: Maybe Int -> String-minCountString (Just x) = "min_count=" ++ show x+minCountString (Just x) = "min_count=" ++ (urlEncode $ show x) minCountString Nothing  = ""
Data/Factual/Query/ReadQuery.hs view
@@ -16,6 +16,7 @@ import Data.Factual.Shared.Filter import Data.Factual.Shared.Geo import Data.Factual.Utils+import Network.HTTP.Base (urlEncode)  -- | The ReadQuery type is used to construct read queries. A table should be --   specified, but the rest of the query options are essentially optional@@ -48,5 +49,5 @@  -- The following helper functions are used in generating query Strings. offsetString :: Maybe Int -> String-offsetString (Just x) = "offset=" ++ show x+offsetString (Just x) = "offset=" ++ (urlEncode $ show x) offsetString Nothing = ""
Data/Factual/Query/ResolveQuery.hs view
@@ -9,6 +9,7 @@  import Data.Factual.Query import Data.Factual.Utils+import Network.HTTP.Base (urlEncode)  -- | A resolve value can either be a String or a Number (Double). The first --   argument is the name of the field and the second argument is the input@@ -30,6 +31,5 @@ -- ResolveQuery is a member of the Query typeclass so that it can be used to -- make requests. instance Query ResolveQuery where-  toPath (ResolveQuery values) = "/places/resolve?values={"-                               ++ (join "," $ map show values)-                               ++ "}"+  toPath (ResolveQuery values) = "/places/resolve?values="+                               ++ urlEncode ("{" ++ (join "," $ map show values) ++ "}")
Data/Factual/Response.hs view
@@ -26,17 +26,20 @@ -- | A response object has a status (that will be ok if the query was successful --   and error if the query failed), a version (which should always be 3.0) and --   the actual response data which is an Aeson value.-data Response = Response { status   :: String-                         , version  :: Double-                         , response :: Value+data Response = Response { status       :: String+                         , version      :: Double+                         , response     :: Value+                         , errorMessage :: Maybe String+                         , errorType    :: Maybe String                          } deriving (Eq, Show)  -- | This function is used by the API module to turn the Aeson value returned by --   the API into a Response value. fromValue :: Value -> Response-fromValue value = Response { status = lookupString "status" value-                           , version = lookupNumber "version" value-                           , response = lookupValue "response" value }+fromValue value+  | respStatus == "error" = formErrorResponse value+  | otherwise             = formValidResponse value+  where respStatus = lookupString "status" value  -- | This function can be used to convert an Aeson Array value into a vanilla --   list.@@ -65,6 +68,19 @@ lookupValueSafe key (Object x) = L.lookup (T.pack key) x  -- The following helper functions aid the lookup functions.+formErrorResponse :: Value -> Response+formErrorResponse value = Response { status = "error"+                                   , version = lookupNumber "version" value+                                   , response = Null+                                   , errorMessage = Just $ lookupString "message" value+                                   , errorType = Just $ lookupString "error_type" value }++formValidResponse :: Value -> Response+formValidResponse value = Response { status = "ok"+                                   , version = lookupNumber "version" value+                                   , response = lookupValue "response" value+                                   , errorMessage = Nothing+                                   , errorType = Nothing } extractString :: Value -> String extractString (String x) = T.unpack x 
Data/Factual/Shared/Filter.hs view
@@ -9,6 +9,7 @@   ) where  import Data.Factual.Utils+import Network.HTTP.Base (urlEncode)  -- | A Field is a String representation of the field name. type Field = String@@ -57,4 +58,4 @@  filtersString :: [Filter] -> String filtersString [] = ""-filtersString fs = "filters={" ++ (join "," $ map show fs) ++ "}"+filtersString fs = "filters=" ++ urlEncode ("{" ++ (join "," $ map show fs) ++ "}")
Data/Factual/Shared/Geo.hs view
@@ -10,6 +10,8 @@   , geoString   ) where +import Network.HTTP.Base (urlEncode)+ -- | A Lat is the latitude represented as a Double. type Lat = Double -- | A Long is the longitude represented as a Double.@@ -41,5 +43,5 @@  -- Helper functions geoString :: Maybe Geo -> String-geoString (Just c) = "geo=" ++ show c+geoString (Just g) = "geo=" ++ (urlEncode $ show g) geoString Nothing = ""
Data/Factual/Shared/Search.hs view
@@ -8,6 +8,7 @@   ) where  import Data.Factual.Utils+import Network.HTTP.Base (urlEncode)  -- | This type is used to construct an ANDed or ORed search in a query. data Search = AndSearch [String] | OrSearch [String] | NoSearch deriving Eq@@ -23,4 +24,4 @@ searchString :: Search -> String searchString (AndSearch []) = "" searchString (OrSearch []) = ""-searchString search = "q=" ++ show search+searchString search = "q=" ++ (urlEncode $ show search)
Data/Factual/Shared/Table.hs view
@@ -6,16 +6,25 @@      Table(..)   ) where --- | Three Table values are currently available for the Factual API.+-- | This type defines the available tables. Use the Custom table option for+--   tables that are not listed you. data Table = Places            | USRestaurants            | Global+           | HealthCareProviders+           | WorldGeographies+           | ProductsCPG+           | ProductsCrosswalk            | Custom String            deriving Eq  -- Table is a member of the Show typeclass to generate the beginning of the path. instance Show Table where-  show Places        = "/t/places/"-  show USRestaurants = "/t/restaurants-us/"-  show Global        = "/t/global/"-  show (Custom name) = "/t/" ++ name ++ "/"+  show Places              = "/t/places/"+  show USRestaurants       = "/t/restaurants-us/"+  show Global              = "/t/global/"+  show HealthCareProviders = "/t/health-care-providers-us"+  show WorldGeographies    = "/t/world-geographies"+  show ProductsCPG         = "/t/products-cpg"+  show ProductsCrosswalk   = "/t/products-crosswalk"+  show (Custom name)       = "/t/" ++ name ++ "/"
Data/Factual/Utils.hs view
@@ -10,6 +10,7 @@   ) where  import Data.List (intersperse)+import Network.HTTP.Base (urlEncode)  -- | The join function joins a list of lists into a list using a separator list. --   The most common use case is for joining Strings with a common separator@@ -25,10 +26,10 @@ -- The following helper functions are used in generating query Strings. selectString :: [String] -> String selectString []      = ""-selectString selects = "select=" ++ (join "," selects)+selectString selects = "select=" ++ (urlEncode $ (join "," selects))  limitString :: Maybe Int -> String-limitString (Just x) = "limit=" ++ show x+limitString (Just x) = "limit=" ++ (urlEncode $ show x) limitString Nothing = ""  includeCountString :: Bool -> String
Network/Factual/API.hs view
@@ -7,13 +7,16 @@     -- * Read functions   , makeRequest   , makeRawRequest+  , makeMultiRequest     -- * Write functions   , sendWrite     -- * The hoauth Token type   , Token(..)   ) where +import Network.HTTP.Base (urlEncode) import Data.Maybe (fromJust)+import Data.List (intersperse) import Network.OAuth.Consumer import Network.OAuth.Http.Request (Request(..), Method(..), parseURL, fromList) import Network.OAuth.Http.Response (Response(..))@@ -21,6 +24,8 @@ import Data.Aeson (Value, decode) import Data.Factual.Query import Data.Factual.Write+import Data.Factual.Utils+import qualified Data.Map as M import qualified Data.ByteString.Lazy.Char8 as B import qualified Data.Factual.Response as F @@ -44,24 +49,45 @@ --   in your Token and the path of your request (e.g. \"\/t\/places?q=starbucks\") makeRawRequest :: Token -> String -> IO F.Response makeRawRequest token queryString = do-  let fullpath = basePath ++ queryString-  let request = generateRequest fullpath-  makeRequest' token request+  response <- makeRawRequest' token queryString+  return $ F.fromValue $ extractJSON response +-- | This function can be used to make multi queries. You pass in a Map of Strings+--   to queries and a single query is made to the API. The result is a Map of the+--   same keys to regular response values.+makeMultiRequest :: (Query query) => Token -> M.Map String query -> IO (M.Map String F.Response)+makeMultiRequest token mult = do+  let queryString = multiQueryString $ M.toList mult+  response <- makeRawRequest' token queryString+  return $ formMultiResponse response $ M.keys mult+ -- | This function takes an OAuth token and a Write and retunrs and IO action --   which sends the Write to API and returns a Response.-sendWrite :: (Write write) => Token -> write -> IO F.Response+sendWrite :: (Write write) => Token -> write -> IO Response sendWrite token write = do   let fullpath = basePath ++ path write   let request = generatePostRequest fullpath (body write)   makeRequest' token request  -- The following helper functions aid the exported API functions-makeRequest' :: Token -> Request -> IO F.Response-makeRequest' token request = do-  response <- runOAuthM token $ setupOAuth request-  return $ F.fromValue $ extractJSON response+makeRawRequest' :: Token -> String -> IO Response+makeRawRequest' token queryString = do+  let fullpath = basePath ++ queryString+  let request = generateRequest fullpath+  makeRequest' token request +makeRequest' :: Token -> Request -> IO Response+makeRequest' token request = runOAuthM token $ setupOAuth request++multiQueryString :: (Query query) => [(String, query)] -> String+multiQueryString ps = "/multi?queries=" ++ (urlEncode $ "{" ++ (join "," $ map queryPair ps) ++ "}")+  where queryPair (n,q) = "\"" ++ n ++ "\":\"" ++ toPath q ++ "\""++formMultiResponse :: Response -> [String] -> M.Map String F.Response+formMultiResponse res ks = M.fromList $ map formPair ks+  where formPair k = (k, F.fromValue $ F.lookupValue k json)+        json       = extractJSON res+ generateRequest :: String -> Request generateRequest url = (fromJust $ parseURL url) { reqHeaders = (fromList headersList) } @@ -80,7 +106,7 @@ extractJSON = fromJust . decode . rspPayload  headersList :: [(String, String)]-headersList = [("X-Factual-Lib", "factual-haskell-driver-0.2.0")]+headersList = [("X-Factual-Lib", "factual-haskell-driver-0.3.0")]  basePath :: String basePath = "http://api.v3.factual.com"
README.md view
@@ -6,25 +6,21 @@  ## Prerequisites -This driver was developed on [ghc 7.0.4](http://www.haskell.org/ghc/)-and [The Haskell Platform 2011.4.0.0](http://hackage.haskell.org/platform/).+This driver targets [ghc 7.4.1](http://www.haskell.org/ghc/)+and [The Haskell Platform 2012.2.0.0](http://hackage.haskell.org/platform/). Please follow the installation instructions for your specific platform to install ghc and The Haskell Platform. +## Installation from cabal++    $ cabal install factual-api+ ## Installation from git      $ git clone git@github.com:rudyl313/factual-haskell-driver.git     $ cabal install hoauth     $ cabal install aeson -Note you may require libcurl to install hoauth. On lucid you can run:--    $ sudo apt-get install libcurl4-openssl-dev--## Installation from cabal--    $ cabal install factual-api- # Tests  Load the tests file into ghci to run the tests:@@ -38,13 +34,13 @@     Cases: 41  Tried: 41  Errors: 0  Failures: 0     Counts {cases = 41, tried = 41, errors = 0, failures = 0}     *Main> runIntegrationTests "mykey" "mysecret"-    Cases: 8  Tried: 8  Errors: 0  Failures: 0-    Counts {cases = 8, tried = 8, errors = 0, failures = 0}+    Cases: 10  Tried: 10  Errors: 0  Failures: 0+    Counts {cases = 10, tried = 10, errors = 0, failures = 0}  # Documentation  You can read library documentation by opening docs/index.html in-your browser.+your browser or by visiting the [Hackage page](http://hackage.haskell.org/package/factual-api).  # Examples @@ -57,3 +53,10 @@ In this example replace mykey with your key and mysecret with your secret. Note that compiling the source code generates .o and .hi files in the source directories.++# Writes++Although there is code in the current version to support API writes,+these features have not yet been implemented in the Factual API. Please+refrain from attempting to use these features until they have been+officially announced.
examples/CrosswalkExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Query.CrosswalkQuery import Data.Factual.Response
examples/FacetsExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Query.FacetsQuery import Data.Factual.Response
examples/GeocodeExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Query.GeocodeQuery import Data.Factual.Response
examples/GeopulseExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Query.GeopulseQuery import Data.Factual.Response
+ examples/MultiExample.hs view
@@ -0,0 +1,42 @@+module Main where++import System.Environment (getArgs)+import Network.Factual.API+import Data.Factual.Query.ReadQuery+import Data.Factual.Response+import qualified Data.Map as M++main :: IO()+main = do+  args <- getArgs+  let oauthKey = head args+  let oauthSecret = last args+  let token = generateToken oauthKey oauthSecret+  let query1 = ReadQuery { table = Places+                         , search = AndSearch []+                         , select = ["name"]+                         , limit = Just 50+                         , offset = Nothing+                         , includeCount = True+                         , geo = Just (Circle 34.06021 (-118.41828) 5000.0)+                         , filters = [EqualStr "name" "Stand"] }+  let query2 = ReadQuery { table = Places+                         , search = AndSearch []+                         , select = ["name"]+                         , limit = Just 50+                         , offset = Nothing+                         , includeCount = True+                         , geo = Just (Circle 34.06021 (-118.41828) 5000.0)+                         , filters = [EqualStr "name" "Xerox"] }+  let multiQuery = M.fromList [("query1", query1), ("query2", query2)]+  multiResult <- makeMultiRequest token multiQuery+  let result1 = multiResult M.! "query1"+  let result2 = multiResult M.! "query2"+  putStrLn "query1:"+  putStrLn $ "Status: " ++ status result1+  putStrLn $ "Version: " ++ show (version result1)+  putStrLn $ "Data: " ++ show (response result1)+  putStrLn "query2:"+  putStrLn $ "Status: " ++ status result2+  putStrLn $ "Version: " ++ show (version result2)+  putStrLn $ "Data: " ++ show (response result2)
examples/RawReadExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Response 
examples/ReadExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Query.ReadQuery import Data.Factual.Response
examples/ResolveExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Query.ResolveQuery import Data.Factual.Response
examples/SchemaExample.hs view
@@ -1,6 +1,6 @@ module Main where -import System (getArgs)+import System.Environment (getArgs) import Network.Factual.API import Data.Factual.Query.SchemaQuery import Data.Factual.Response
factual-api.cabal view
@@ -1,5 +1,5 @@ name:            factual-api-version:         0.2.0+version:         0.3.0 license:         BSD3 license-file:    LICENSE.txt category:        API, Web@@ -7,7 +7,7 @@ author:          Rudiger Lippert <rudy@factual.com> maintainer:      Rudiger Lippert <rudy@factual.com> stability:       stable-tested-with:     GHC == 7.0.4+tested-with:     GHC == 7.4.1 synopsis:        A driver for the Factual API cabal-version:   >= 1.8 homepage:        https://github.com/rudyl313/factual-haskell-driver@@ -50,7 +50,10 @@     aeson >= 0.6.0.0,     attoparsec >= 0.10.1.0,     base == 4.*,+    bytestring,+    containers,     hoauth >= 0.3.0,+    HTTP,     text >= 0.11.1.5,     unordered-containers >= 0.1.4.6,     vector >= 0.9.1
test/Tests.hs view
@@ -44,7 +44,7 @@                      , TestLabel "Is blank filter test" isBlankFilterTest                      , TestLabel "Is not blank filter test" isNotBlankFilterTest                      , TestLabel "And filter test" andFilterTest-                     , TestLabel "Or filter test" andFilterTest+                     , TestLabel "Or filter test" orFilterTest                      , TestLabel "Geo test" geoTest                      , TestLabel "Include count test" includeCountTest                      , TestLabel "Schema query test" schemaQueryTest@@ -67,7 +67,9 @@                                        , TestLabel "Raw read test" (rawIntegrationTest token)                                        , TestLabel "Facets test" (facetsIntegrationTest token)                                        , TestLabel "Geopulse test" (geopulseIntegrationTest token)-                                       , TestLabel "Geocode test" (geocodeIntegrationTest token) ]+                                       , TestLabel "Geocode test" (geocodeIntegrationTest token)+                                       , TestLabel "Multi test" (multiIntegrationTest token)+                                       , TestLabel "Error test" (errorIntegrationTest token) ]                             where token = generateToken key secret  placeTablePathTest = TestCase (do@@ -91,27 +93,27 @@   assertEqual "Correct path for custom table" expected path)  geopulsePathTest = TestCase (do-  let expected = "/places/geopulse?geo={\"$point\":[34.06021,-118.41828]}&select=commercial_density"+  let expected = "/places/geopulse?geo=%7B%22%24point%22%3A%5B34.06021%2C-118.41828%5D%7D&select=commercial_density"   let path = toPath $ G.GeopulseQuery { G.geo = Point 34.06021 (-118.41828) , G.select = ["commercial_density"] }   assertEqual "Correct path for geopulse" expected path)  geocodePathTest = TestCase (do-  let expected = "/places/geocode?geo={\"$point\":[34.06021,-118.41828]}"+  let expected = "/places/geocode?geo=%7B%22%24point%22%3A%5B34.06021%2C-118.41828%5D%7D"   let path = toPath $ GeocodeQuery $ Point 34.06021 (-118.41828)   assertEqual "Correct path for geocode" expected path)  andSearchPathTest = TestCase (do-  let expected = "/t/places/read?q=foo bar&include_count=false"+  let expected = "/t/places/read?q=foo%20bar&include_count=false"   let path = toPath $ blankReadQuery { search = AndSearch ["foo", "bar"] }   assertEqual "Correct path for ANDed search" expected path)  orSearchPathTest = TestCase (do-  let expected = "/t/places/read?q=foo,bar&include_count=false"+  let expected = "/t/places/read?q=foo%2Cbar&include_count=false"   let path = toPath $ blankReadQuery { search = OrSearch ["foo", "bar"] }   assertEqual "Correct path for ANDed search" expected path)  selectPathTest = TestCase (do-  let expected = "/t/places/read?select=foo,bar&include_count=false"+  let expected = "/t/places/read?select=foo%2Cbar&include_count=false"   let path = toPath $ blankReadQuery { select = ["foo", "bar"] }   assertEqual "Correct path for select terms" expected path) @@ -126,87 +128,87 @@   assertEqual "Correct path for offset" expected path)  equalNumFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":123.4}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A123.4%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [EqualNum "field" 123.4] }   assertEqual "Correct path for equal number filter" expected path)  equalStrFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":\"value\"}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%22value%22%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [EqualStr "field" "value"] }   assertEqual "Correct path for equal string filter" expected path)  notEqualNumFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$neq\":123.4}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24neq%22%3A123.4%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [NotEqualNum "field" 123.4] }   assertEqual "Correct path for not equal number filter" expected path)  notEqualStrFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$neq\":\"value\"}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24neq%22%3A%22value%22%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [NotEqualStr "field" "value"] }   assertEqual "Correct path for not equal string filter" expected path)  inNumListFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$in\":[123.4,5432.1]}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24in%22%3A%5B123.4%2C5432.1%5D%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [InNumList "field" [123.4, 5432.1]] }   assertEqual "Correct path for in number list filter" expected path)  inStrListFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$in\":[\"value\",\"other\"]}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24in%22%3A%5B%22value%22%2C%22other%22%5D%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [InStrList "field" ["value","other"]] }   assertEqual "Correct path for in string list filter" expected path)  notInNumListFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$nin\":[123.4,5432.1]}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24nin%22%3A%5B123.4%2C5432.1%5D%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [NotInNumList "field" [123.4, 5432.1]] }   assertEqual "Correct path for not in number list filter" expected path)  notInStrListFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$nin\":[\"value\",\"other\"]}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24nin%22%3A%5B%22value%22%2C%22other%22%5D%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [NotInStrList "field" ["value","other"]] }   assertEqual "Correct path for not in string list filter" expected path)  beginsWithFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$bw\":\"val\"}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24bw%22%3A%22val%22%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [BeginsWith "field" "val"] }   assertEqual "Correct path for begins with filter" expected path)  notBeginsWithFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$nbw\":\"val\"}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24nbw%22%3A%22val%22%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [NotBeginsWith "field" "val"] }   assertEqual "Correct path for not begins with filter" expected path)  beginsWithAnyFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$bwin\":[\"val\",\"ot\"]}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24bwin%22%3A%5B%22val%22%2C%22ot%22%5D%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [BeginsWithAny "field" ["val","ot"]] }   assertEqual "Correct path for begins with any filter" expected path)  notBeginsWithAnyFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$nbwin\":[\"val\",\"ot\"]}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24nbwin%22%3A%5B%22val%22%2C%22ot%22%5D%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [NotBeginsWithAny "field" ["val","ot"]] }   assertEqual "Correct path for not begins with any filter" expected path)  isBlankFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$blank\":true}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24blank%22%3Atrue%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [IsBlank "field"] }   assertEqual "Correct path for is blank filter" expected path)  isNotBlankFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"field\":{\"$blank\":false}}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22field%22%3A%7B%22%24blank%22%3Afalse%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [IsNotBlank "field"] }   assertEqual "Correct path for is not blank filter" expected path)  andFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"$and\":[{\"field1\":{\"$blank\":true}},{\"field2\":{\"$blank\":false}}]}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22%24and%22%3A%5B%7B%22field1%22%3A%7B%22%24blank%22%3Atrue%7D%7D%2C%7B%22field2%22%3A%7B%22%24blank%22%3Afalse%7D%7D%5D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [And [IsBlank "field1", IsNotBlank "field2"]] }   assertEqual "Correct path for and filter" expected path)  orFilterTest = TestCase (do-  let expected = "/t/places/read?filters={\"$or\":[{\"field1\":{\"$blank\":true}},{\"field2\":{\"$blank\":false}}]}&include_count=false"+  let expected = "/t/places/read?filters=%7B%22%24or%22%3A%5B%7B%22field1%22%3A%7B%22%24blank%22%3Atrue%7D%7D%2C%7B%22field2%22%3A%7B%22%24blank%22%3Afalse%7D%7D%5D%7D&include_count=false"   let path = toPath $ blankReadQuery { filters = [Or [IsBlank "field1", IsNotBlank "field2"]] }   assertEqual "Correct path for or filter" expected path)  geoTest = TestCase (do-  let expected = "/t/places/read?geo={\"$circle\":{\"$center\":[300.1, 200.3],\"$meters\":100.5}}&include_count=false"+  let expected = "/t/places/read?geo=%7B%22%24circle%22%3A%7B%22%24center%22%3A%5B300.1%2C%20200.3%5D%2C%22%24meters%22%3A100.5%7D%7D&include_count=false"   let path = toPath $ blankReadQuery { geo = Just (Circle 300.1 200.3 100.5) }   assertEqual "Correct path for geo" expected path) @@ -221,7 +223,7 @@   assertEqual "Correct path for a schema query" expected path)  resolveQueryTest = TestCase (do-  let expected = "/places/resolve?values={\"field1\":\"value1\",\"field2\":32.1}"+  let expected = "/places/resolve?values=%7B%22field1%22%3A%22value1%22%2C%22field2%22%3A32.1%7D"   let path = toPath $ ResolveQuery [ResolveStr "field1" "value1", ResolveNum "field2" 32.1]   assertEqual "Correct path for a resolve query" expected path) @@ -246,14 +248,12 @@   assertEqual "Correct path for a namespace id" expected path)  onlyTest = TestCase (do-  let expected = "/places/crosswalk?only=yelp,loopd"+  let expected = "/places/crosswalk?only=yelp%2Cloopd"   let path = toPath $ blankCrosswalkQuery { C.only = ["yelp", "loopd"] }   assertEqual "Correct path for a only" expected path)  facetsTest = TestCase (do-  let expected = "/t/places/facets?q=starbucks&select=locality,region&"-               ++ "filters={\"country\":\"US\"}&limit=10&min_count=2&"-               ++ "include_count=false"+  let expected = "/t/places/facets?q=starbucks&select=locality%2Cregion&filters=%7B%22country%22%3A%22US%22%7D&limit=10&min_count=2&include_count=false"   let path = toPath $ F.FacetsQuery { F.table        = Places                                     , F.search       = AndSearch ["starbucks"]                                     , F.select       = ["locality", "region"]@@ -346,6 +346,28 @@   let query = GeocodeQuery $ Point 34.06021 (-118.41828)   result <- makeRequest token query   assertEqual "Valid geopulse query" "ok" (status result))+++multiIntegrationTest :: Token -> Test+multiIntegrationTest token = TestCase (do+  let query1 = ReadQuery { table = Places+                         , search = AndSearch ["McDonalds", "Burger King"]+                         , select = ["name"]+                         , limit = Just 50+                         , offset = Just 10+                         , includeCount = True+                         , geo = Just (Circle 34.06021 (-118.41828) 5000.0)+                         , filters = [EqualStr "name" "Stand"] }+  let query2 = query1 { filters = [EqualStr "name" "Xerox"] }+  results <- makeMultiRequest token $ M.fromList [("query1", query1), ("query2", query2)]+  let result1 = results M.! "query1"+  let result2 = results M.! "query2"+  assertEqual "Valid multi query" ["ok","ok"] [status result1, status result2])++errorIntegrationTest :: Token -> Test+errorIntegrationTest token = TestCase (do+  result <- makeRawRequest token "/t/foobarbaz"+  assertEqual "Invalud read query" "error" (status result))  blankReadQuery :: ReadQuery blankReadQuery = ReadQuery { table = Places