|
| 1 | +{-# LANGUAGE TypeFamilies #-} |
| 2 | +{-# LANGUAGE MultiParamTypeClasses #-} |
| 3 | +{-# LANGUAGE FlexibleInstances #-} |
| 4 | +{-# LANGUAGE ExistentialQuantification #-} |
| 5 | +{-# LANGUAGE FunctionalDependencies #-} |
| 6 | +{-# LANGUAGE ScopedTypeVariables #-} |
| 7 | +{-# LANGUAGE GADTs #-} |
| 8 | +{-# LANGUAGE DataKinds #-} |
| 9 | +{-# LANGUAGE TypeOperators #-} |
| 10 | +{-# LANGUAGE ScopedTypeVariables #-} |
| 11 | +{-# LANGUAGE RankNTypes #-} |
| 12 | +{-# LANGUAGE TypeInType #-} |
| 13 | +{-# LANGUAGE TypeApplications #-} |
| 14 | +{-# LANGUAGE LambdaCase #-} |
| 15 | +{-# LANGUAGE OverloadedStrings #-} |
| 16 | +{-# LANGUAGE DeriveGeneric #-} |
| 17 | +{-# LANGUAGE DefaultSignatures #-} |
| 18 | +{-# LANGUAGE FlexibleContexts #-} |
| 19 | +{-# LANGUAGE JavaScriptFFI #-} |
| 20 | +{-# LANGUAGE RecordWildCards #-} |
| 21 | +module Solga.Client.GHCJS where |
| 22 | + {- |
| 23 | + ( Client(..) |
| 24 | + , SomeRequestData(..) |
| 25 | + , choose |
| 26 | + , RawRequest(..) |
| 27 | + , ToSegment(..) |
| 28 | + , WithData(..) |
| 29 | + , GetResponse(..) |
| 30 | + ) where |
| 31 | + -} |
| 32 | + |
| 33 | +import Data.Kind |
| 34 | +import Data.Proxy |
| 35 | +import GHC.Generics |
| 36 | +import GHC.TypeLits (symbolVal, KnownSymbol, Symbol) |
| 37 | +import Data.Monoid ((<>)) |
| 38 | +import qualified JavaScript.Web.XMLHttpRequest as Xhr |
| 39 | +import Control.Exception (Exception, throwIO) |
| 40 | +import qualified Data.JSString as JSS |
| 41 | +import Data.JSString (JSString) |
| 42 | +import Data.Typeable (Typeable) |
| 43 | +import qualified Data.DList as DList |
| 44 | +import Data.DList (DList) |
| 45 | +import Data.String (fromString) |
| 46 | +import qualified JavaScript.JSON.Types.Class as Json |
| 47 | +import qualified JavaScript.JSON.Types.Internal as Json |
| 48 | +import GHCJS.Types (Immutable) |
| 49 | + |
| 50 | +import Solga.Core hiding (Header) |
| 51 | + |
| 52 | +data SomeRequestData out a = forall in_. (Client in_) => SomeRequestData (Proxy in_) (RequestData in_ a) |
| 53 | + |
| 54 | +type Header = (JSString, JSString) |
| 55 | + |
| 56 | +data Request = Request |
| 57 | + { reqMethod :: String |
| 58 | + , reqHost :: JSString |
| 59 | + , reqSegments :: DList JSString |
| 60 | + , reqQueryString :: JSString |
| 61 | + , reqData :: Xhr.RequestData |
| 62 | + , reqLogin :: Maybe (JSString, JSString) |
| 63 | + , reqHeaders :: [Header] |
| 64 | + , reqWithCredentials :: Bool |
| 65 | + } |
| 66 | + |
| 67 | +newtype BadMethod = BadMethod String |
| 68 | + deriving (Eq, Show, Typeable) |
| 69 | +instance Exception BadMethod |
| 70 | + |
| 71 | +foreign import javascript unsafe |
| 72 | + "encodeURI($1)" |
| 73 | + js_encodeURI :: JSString -> IO JSString |
| 74 | + |
| 75 | +toXhrRequest :: Request -> IO Xhr.Request |
| 76 | +toXhrRequest Request{..} = do |
| 77 | + meth <- case reqMethod of |
| 78 | + "GET" -> return Xhr.GET |
| 79 | + "POST" -> return Xhr.POST |
| 80 | + "PUT" -> return Xhr.PUT |
| 81 | + "DELETE" -> return Xhr.DELETE |
| 82 | + x -> throwIO (BadMethod x) |
| 83 | + uri <- js_encodeURI (reqHost <> "/" <> JSS.intercalate "/" (DList.toList reqSegments) <> reqQueryString) |
| 84 | + return Xhr.Request |
| 85 | + { Xhr.reqMethod = meth |
| 86 | + , Xhr.reqURI = uri |
| 87 | + , Xhr.reqLogin = reqLogin |
| 88 | + , Xhr.reqHeaders = reqHeaders |
| 89 | + , Xhr.reqWithCredentials = reqWithCredentials |
| 90 | + , Xhr.reqData = reqData |
| 91 | + } |
| 92 | + |
| 93 | +class Client r where |
| 94 | + type RequestData r :: * -> * |
| 95 | + type RequestData r = SomeRequestData r |
| 96 | + performRequest :: proxy r -> Request -> RequestData r a -> IO a |
| 97 | + default |
| 98 | + performRequest :: forall (proxy :: * -> *) a. |
| 99 | + (RequestData r ~ SomeRequestData r) |
| 100 | + => proxy r -> Request -> RequestData r a -> IO a |
| 101 | + performRequest _p req (SomeRequestData p perf) = performRequest p req perf |
| 102 | + |
| 103 | +choose :: forall in_ out a. |
| 104 | + (Client in_, RequestData out ~ SomeRequestData out) |
| 105 | + => (out -> in_) -> RequestData in_ a -> RequestData out a |
| 106 | +choose _f perf = SomeRequestData (Proxy @in_) perf |
| 107 | + |
| 108 | +newtype RawRequest a = RawRequest {unRequestDataRaw :: Request -> IO a} |
| 109 | + |
| 110 | +instance Client (Raw a) where |
| 111 | + type RequestData (Raw a) = RawRequest |
| 112 | + performRequest _p req (RawRequest f) = f req |
| 113 | + |
| 114 | +instance Client (RawResponse a) where |
| 115 | + type RequestData (RawResponse a) = RawRequest |
| 116 | + performRequest _p req (RawRequest f) = f req |
| 117 | + |
| 118 | +instance (Client next) => Client (End next) where |
| 119 | + type RequestData (End next) = RequestData next |
| 120 | + performRequest _p req perf = performRequest (Proxy @next) req perf |
| 121 | + |
| 122 | +addSegment :: Request -> JSString -> Request |
| 123 | +addSegment req seg = req{reqSegments = reqSegments req <> DList.singleton seg} |
| 124 | + |
| 125 | +instance (Client next, KnownSymbol seg) => Client (Seg seg next) where |
| 126 | + type RequestData (Seg seg next) = RequestData next |
| 127 | + performRequest _p req perf = |
| 128 | + performRequest (Proxy @next) (addSegment req (fromString (symbolVal (Proxy @seg)))) perf |
| 129 | + |
| 130 | +instance (Client left, Client right) => Client (left :<|> right) where |
| 131 | + type RequestData (left :<|> right) = RequestData left :+: RequestData right |
| 132 | + performRequest _p req = \case |
| 133 | + L1 perf -> performRequest (Proxy @left) req perf |
| 134 | + R1 perf -> performRequest (Proxy @right) req perf |
| 135 | + |
| 136 | +data WhichSeg (segs :: [Symbol]) where |
| 137 | + ThisSeg :: KnownSymbol seg => WhichSeg (seg ': segs) |
| 138 | + ThatSeg :: WhichSeg segs -> WhichSeg (seg ': segs) |
| 139 | + |
| 140 | +thisSeg :: forall seg segs. KnownSymbol seg => WhichSeg (seg ': segs) -> String |
| 141 | +thisSeg _ = symbolVal (Proxy @seg) |
| 142 | + |
| 143 | +whichSeg :: WhichSeg segs -> String |
| 144 | +whichSeg ts@ThisSeg = thisSeg ts |
| 145 | +whichSeg (ThatSeg ws) = whichSeg ws |
| 146 | + |
| 147 | +instance (Client next) => Client (OneOfSegs segs next) where |
| 148 | + type RequestData (OneOfSegs segs next) = WithData (WhichSeg segs) (RequestData next) |
| 149 | + performRequest _p req (WithData ws perf) = |
| 150 | + performRequest (Proxy @next) (addSegment req (fromString (whichSeg ws))) perf |
| 151 | + |
| 152 | +class ToSegment a where |
| 153 | + toSegment :: a -> JSString |
| 154 | + |
| 155 | +instance ToSegment JSString where |
| 156 | + toSegment = id |
| 157 | + |
| 158 | +data WithData a next b = WithData |
| 159 | + { ardData :: a |
| 160 | + , ardNext :: next b |
| 161 | + } |
| 162 | + |
| 163 | +instance (Client next, ToSegment a) => Client (Capture a next) where |
| 164 | + type RequestData (Capture a next) = WithData a (RequestData next) |
| 165 | + performRequest _p req (WithData x perf) = |
| 166 | + performRequest (Proxy @next) (addSegment req (toSegment x)) perf |
| 167 | + |
| 168 | +instance (Client next, KnownSymbol method) => Client (Method method next) where |
| 169 | + type RequestData (Method seg next) = RequestData next |
| 170 | + performRequest _p req perf = performRequest |
| 171 | + (Proxy @next) req{reqMethod = symbolVal (Proxy @method)} perf |
| 172 | + |
| 173 | +newtype GetResponse resp a b = GetResponse {unGetResponse :: Xhr.Response resp -> a -> IO b} |
| 174 | + |
| 175 | +instance (Json.FromJSON a) => Client (JSON a) where |
| 176 | + -- note that we do not decode eagerly because it's often the case that the body |
| 177 | + -- cannot be decoded since web servers return invalid json on errors |
| 178 | + -- (e.g. "Internal server error" on a 500 rather than a json encoded error) |
| 179 | + type RequestData (JSON a) = GetResponse (Json.SomeValue Immutable) (Maybe (Either String a)) |
| 180 | + performRequest _p req (GetResponse f) = do |
| 181 | + resp <- Xhr.xhr =<< toXhrRequest req |
| 182 | + f resp $ do |
| 183 | + data_ <- Xhr.contents resp |
| 184 | + return (Json.parseEither Json.parseJSON data_) |
| 185 | + |
| 186 | +instance (Client next) => Client (ExtraHeaders next) where |
| 187 | + type RequestData (ExtraHeaders next) = RequestData next |
| 188 | + performRequest _p req perf = performRequest (Proxy @next) req perf |
| 189 | + |
| 190 | +instance (Client next) => Client (NoCache next) where |
| 191 | + type RequestData (NoCache next) = RequestData next |
| 192 | + performRequest _p req perf = performRequest (Proxy @next) req perf |
| 193 | + |
| 194 | +instance (Client next, Json.ToJSON a) => Client (ReqBodyJSON a next) where |
| 195 | + type RequestData (ReqBodyJSON a next) = WithData a (RequestData next) |
| 196 | + performRequest _p req (WithData x perf) = performRequest |
| 197 | + (Proxy @next) req{reqData = Xhr.StringData (Json.encode (Json.toJSON x))} perf |
| 198 | + |
| 199 | +instance (Client next) => Client (WithIO next) where |
| 200 | + type RequestData (WithIO next) = RequestData next |
| 201 | + performRequest _p req perf = performRequest (Proxy @next) req perf |
| 202 | + |
| 203 | +instance (Client next) => Client (ReqBodyMultipart fp a next) where |
| 204 | + type |
| 205 | + RequestData (ReqBodyMultipart fp a next) = |
| 206 | + WithData [(JSString, Xhr.FormDataVal)] (RequestData next) |
| 207 | + performRequest _p req (WithData fd perf) = do |
| 208 | + performRequest (Proxy @next) req{reqData = Xhr.FormData fd} perf |
0 commit comments