{-# LANGUAGE OverloadedStrings, Rank2Types #-}
module NationStates.Core (
NS,
makeNS,
makeNS',
requestNS,
apiVersion,
Query(..),
shard,
shard',
withOptions,
withParams,
Context(..),
wordsBy,
readMaybe,
expect,
pureIf,
module NationStates.Types,
) where
import Control.Applicative
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BC
import Data.Functor.Compose
import qualified Data.Foldable as F
import Data.List
import Data.Monoid
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe
import Data.Set (Set)
import qualified Data.Set as Set
import Network.HTTP.Client
import qualified Network.HTTP.Types as HTTP
import Text.Read
import Text.XML.Light
import Prelude
import NationStates.Types
type NS = Compose ((,) Query) (Compose ((->) Query) ((->) Element))
makeNS
:: String
-> String
-> NS String
makeNS shardName elemName = makeNS' (shard shardName) parse
where
parse _ = strContent . fromMaybe errorMissing . findChild (unqual elemName)
errorMissing = error $ "missing <" ++ elemName ++ "> element"
makeNS'
:: Query
-> (Query -> Element -> a)
-> NS a
makeNS' query parse = Compose (query, Compose parse)
requestNS
:: Maybe (String, String)
-> NS a
-> Context
-> IO a
requestNS kindAndName (Compose (q, Compose p)) c
= parse . responseBody <$>
(contextRateLimit c $ httpLbs req (contextManager c))
where
parse = p q . fromMaybe (error "invalid response") . parseXMLDoc
req = initRequest {
queryString = queryToString kindAndName q,
requestHeaders
= ("User-Agent", BC.pack $ contextUserAgent c)
: requestHeaders initRequest,
port = if contextIsSecure c then 443 else 80,
secure = contextIsSecure c
}
initRequest :: Request
Just initRequest = parseUrl "http://www.nationstates.net/cgi-bin/api.cgi"
apiVersion :: Integer
apiVersion = 7
data Context = Context {
contextManager :: Manager,
contextRateLimit :: forall a. IO a -> IO a,
contextIsSecure :: Bool,
contextUserAgent :: String
}
data Query = Query {
queryShards :: Map String (Set (Maybe Integer)),
queryOptions :: Map String String,
queryParams :: Map String String
} deriving Show
instance Monoid Query where
mempty = Query mempty mempty mempty
mappend a b = Query {
queryShards = Map.unionWith Set.union
(queryShards a) (queryShards b),
queryOptions = Map.unionWithKey mergeOptions
(queryOptions a) (queryOptions b),
queryParams = Map.unionWithKey mergeOptions
(queryParams a) (queryParams b)
}
where
mergeOptions key _ _
= error $ "conflicting values for option " ++ show key
shard :: String -> Query
shard name = mempty {
queryShards = Map.singleton name (Set.singleton Nothing) }
shard' :: String -> Integer -> Query
shard' name id' = mempty {
queryShards = Map.singleton name (Set.singleton (Just id')) }
withOptions :: [(String, String)] -> Query
withOptions options = mempty { queryOptions = Map.fromList options }
withParams :: [(String, String)] -> Query
withParams params = mempty { queryParams = Map.fromList params }
queryToString :: Maybe (String, String) -> Query -> ByteString
queryToString kindAndName q
= HTTP.renderQuery True (HTTP.toQuery $
F.toList kindAndName
++ Map.toList (queryParams q)
++ [("q", shards), ("v", show apiVersion)])
<> BC.pack options
where
shards
| Map.null (queryShards q) = "null"
| otherwise
= intercalate "+" [ name ++ F.foldMap (\i -> "-" ++ show i) maybeId |
(name, is) <- Map.toList $ queryShards q,
maybeId <- Set.toList is ]
options = concat [ ";" ++ k ++ "=" ++ v |
(k, v) <- Map.toList $ queryOptions q ]
wordsBy :: (a -> Bool) -> [a] -> [[a]]
wordsBy p s = case dropWhile p s of
[] -> []
s' ->
let (w, s'') = break p s'
in w : wordsBy p s''
expect :: String -> String -> Maybe a -> a
expect want got = fromMaybe (error $ "expected " ++ want ++ " but got: " ++ got)
pureIf :: Alternative f => (a -> Bool) -> a -> f a
pureIf p x = if p x then pure x else empty