packages feed

react-flux-servant (empty) → 0.1.0

raw patch · 6 files changed

+367/−0 lines, 6 filesdep +aesondep +basedep +ghcjs-basesetup-changed

Dependencies added: aeson, base, ghcjs-base, react-flux, servant, text

Files

+ ChangeLog.md view
@@ -0,0 +1,3 @@+# 0.1.0++* Initial release.  See the haddock for documentation.
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2016, John Lenz++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * Neither the name of John Lenz nor the names of other+      contributors may be used to endorse or promote products derived+      from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,15 @@+This project integrates [react-flux](http://hackage.haskell.org/package/react-flux) with+[servant](http://hackage.haskell.org/package/servant).  There is a small example in the+haddock documentation.  To depend on this module, I add it to the `extra-deps` in my+project's `stack.yaml`.  See the [react-flux](http://hackage.haskell.org/package/react-flux)+documentation for a full example `stack.yaml`.++At the moment the code works well and is straightforward to integrate into a react-flux+store; I am using it in production.  Having said that, the current API requires you+to create two store actions for each request and it is somewhat tedious to write all+this boilerplate.  At some point, I would like to build a better API, perhaps one that+takes better advantage of the type-level Servant.API definitions to drastically reduce+the boilerplate.  Unfortunately, since it currently works (and even works well, the existing+API is good), due to time constraints it might be awhile before I get around to thinking about+better APIs.  If you have any ideas for a better API, even if you don't have any code,+please open an issue!
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ react-flux-servant.cabal view
@@ -0,0 +1,44 @@+name:                react-flux-servant+version:             0.1.0+synopsis:            Allow react-flux stores to send requests to a servant server+category:            Web+homepage:            https://bitbucket.org/wuzzeb/react-flux-servant+license:             BSD3+license-file:        LICENSE+author:              John Lenz <wuzzeb@gmail.com>+maintainer:          John Lenz <wuzzeb@gmail.com>+build-type:          Simple+description:         See the README below.+cabal-version:       >=1.10++extra-source-files:+    ChangeLog.md,+    README.md++source-repository head+    type: mercurial+    location: https://bitbucket.org/wuzzeb/react-flux-servant++library+  hs-source-dirs: src+  ghc-options: -Wall+  default-language: Haskell2010+  exposed-modules: React.Flux.Addons.Servant++  default-extensions: CPP+                      TypeOperators+                      DataKinds+                      PolyKinds+                      TypeFamilies+                      FlexibleInstances+                      OverloadedStrings+                      ScopedTypeVariables++  build-depends: base >=4.8 && < 5+               , react-flux >= 1.2 && < 1.3+               , servant >= 0.7+               , aeson >= 0.8+               , text >= 1.2++  if impl(ghcjs)+      build-depends: ghcjs-base
+ src/React/Flux/Addons/Servant.hs view
@@ -0,0 +1,273 @@+-- | Make calls to an API defined using "Servant.API" from your "React.Flux" stores.+-- __You must call 'initAjax' from your main function for this to work.__+-- For now, this module only works with JSON.+--+-- The general pattern I currently use is to create a store action to trigger the request+-- and another store action to process the response.  Only the store action to trigger+-- the request is exported.  This does lead to a bit of tedious boilerplate since you need+-- actions for each request, so I am still searching for a better API that perhaps takes+-- advantage of the servant API type-level computation to reduce the boilerplate.  For now,+-- until I figure out a better way, this direct approach does work.  If you have any ideas+-- for a good API, please open an issue on bitbucket (even if you don't have full code)!+--+-- As an example, say that the API consists of two methods:+--+-- @+-- type GetUser = "user" :> Capture "user_id" UserId :> Get '[JSON] User+-- type SetUser = "user" :> Capture "user_id" UserId :> ReqBody '[JSON] User :> Post '[JSON] ()+-- type MyAPI = GetUser :\<|\> SetUser+-- @+--+-- I would create a store as follows:+--+-- @+-- data RequestStatus = NoPendingRequest | PendingRequest | PreviousRequestHadError String+--+-- data UserStore = UserStore+--   { users :: Map.HashMap UserId User+--   , reqStatus :: RequestStatus+--   }+--+-- data UserStoreAction = RequestUser UserId+--                      | RequestUserResponse UserId (Either (Int,String) User)+--                      | UpdateUser UserId User+--                      | UpdateUserResponse UserId (Either (Int, String) ())+--   deriving (Show, Generic, NFData)+--+-- cfg :: ApiRequestConfig MyAPI+-- cfg = ApiRequestConfig "https://www.example.com" NoTimeout+--+-- instance StoreData UserStore where+--   type StoreAction UserStore = UserStoreAction+--+--   transform (RequestUser uid) us = do+--     request cfg (Proxy :: Proxy GetUser) uid $+--       \\r -> return [SomeStoreAction userStore $ RequestUserResponse uid r]+--     return $ us {reqStatus = PendingRequest}+--+--   transform (RequestUserResponse _ (Left (_errCode, err))) us =+--     return $ us {reqStatus = PreviousRequestHadError err}+--+--   transform (RequestUserResponse uid (Right u)) us =+--     return $ us { reqStatus = NoPendingRequest+--                 , users = Map.insert uid u (users us)+--                 }+--+--   transform (UpdateUser uid u) us = do+--     request cfg (Proxy :: Proxy SetUser) uid u $+--       \\r -> return [SomeStoreAction userStore $ UpdateUserResponse uid r]+--     return $ us { reqStatus = PendingRequest+--                 , users = Map.insert uid u (users us)+--                 }+--+--   transform (UpdateUserResponse uid (Left (_errCode, err))) us =+--     return $ us { reqStatus = PreviousRequestHadError err+--                 , users = Map.delete uid (users us)+--                 }+--+--   transform (UpdateUserResponse _ (Right ())) us =+--     return $ us { reqStatus = NoPendingRequest}+--+-- userStore :: ReactStore UserStore+-- userStore = mkStore $ UserStore Map.empty NoPendingRequest+-- @+module React.Flux.Addons.Servant(+    HandleResponse+  , RequestTimeout(..)+  , ApiRequestConfig(..)+  , request+  , HasAjaxRequest(..)+) where++import React.Flux+import React.Flux.Ajax+import Servant.Utils.Links+import Servant.API+import GHC.TypeLits+import Data.Typeable (Proxy(..))+import Data.Aeson+import Data.Monoid ((<>))+import qualified Data.Text as T+import qualified Data.Text.Encoding as T++#ifdef __GHCJS__+import Data.JSString (JSString)+import GHCJS.Types (JSVal, nullRef)+import GHCJS.Marshal (toJSVal_aeson, FromJSVal(..))+import Data.JSString.Text (textToJSString)+import qualified Data.JSString as JSS++jsPack :: String -> JSString+jsPack = JSS.pack+jsUnpack :: JSString -> String+jsUnpack = JSS.unpack+jsIntercalate :: JSString -> [JSString] -> JSString+jsIntercalate = JSS.intercalate++#else+import Data.List (intercalate)+type JSVal = ()+type JSString = String++nullRef :: JSVal+nullRef = ()++jsPack :: String -> JSString+jsPack = id++jsUnpack :: JSString -> String+jsUnpack = id++jsIntercalate :: JSString -> [JSString] -> JSString+jsIntercalate = intercalate++textToJSString :: T.Text -> JSString+textToJSString = T.unpack++fromJSVal :: JSVal -> IO a+fromJSVal = undefined++toJSVal_aeson :: a -> IO JSString+toJSVal_aeson = undefined++#endif++-- | Internal state used when building the request.+data Request = Request {+    segments :: [JSString]+  , rHeaders :: [(JSString, JSString)]+  , rQuery :: [(JSString, JSString)]+  , rBody :: IO JSVal+  , rTimeout :: RequestTimeout+}++-- | When a response from the server arrives, it is either an error or a valid response.  An error+-- is turned into a 'Left' value consisting of the HTTP response code and the response body.  If+-- a 200 HTTP response is received, it is parsed to a value according to the servant API definition+-- and passed as a 'Right' value.  You must then turn the response into a store action.  I suggest+-- that you just pass the value along directly to a store action without any computation; the computation+-- can happen inside the store.+type HandleResponse a = Either (Int,String) a -> IO [SomeStoreAction]++-- | Settings for requests built using this module.+data ApiRequestConfig api = ApiRequestConfig+    { urlPrefix :: JSString+    -- ^ A prefix for all requests; it should include the domain and any prefix path that is required.+    -- To this prefix a forward slash is appended and then the path built using the Servant API definition.+    , timeout :: RequestTimeout+    -- ^ The timeout to use for requests.  If a timeout occurs, a 'Left' value with code 504 is passed to+    -- 'HandleResponse'.+    }++-- | Make a request to a servant endpoint. __You must call 'initAjax' from your main function for this to work.__+--+-- 'request' takes the 'ApiRequestConfig', a proxy for the endpoint,+-- parameters for the request (request body, query params, path captures, etc.), and value of type 'HandleResponse'.+-- The result of 'request' is then a value of type @IO ()@.  In order to type-check that the proper values for+-- the request body, path captures, etc. are passed, the 'MkRequest' associated type is used.  'MkRequest' expands+-- to a function with one argument for each path piece and an argument for the 'HandleResponse'.  For example,+--+-- @+-- type GetUser = "user" :> Capture "user_id" UserId :> Get '[JSON] User+-- type SetUser = "user" :> Capture "user_id" UserId :> ReqBody '[JSON] User :> Post '[JSON] ()+-- type MyAPI = GetUser :\<|\> SetUser+-- @+--+-- Then+--+-- @+-- MkRequest GetUser ~ UserId -> HandleResponse User -> IO ()+-- MkRequest SetUser ~ UserId -> User -> HandleResponse () -> IO ()+-- @+--+-- so that+--+-- @+-- request cfg (Proxy :: Proxy GetUser) :: UserId -> HandleResponse User -> IO ()+-- request cfg (Proxy :: Proxy SetUser) :: UserId -> User -> HandleResponse () -> IO ()+-- @+request :: (IsElem endpoint api, HasAjaxRequest endpoint) => ApiRequestConfig api -> Proxy endpoint -> MkRequest endpoint+request (ApiRequestConfig p t) endpoint = toRequest endpoint (Request [p] [] [] (pure nullRef) t)++-- | A class very similar to "Servant.Utils.Links".  You shouldn't need to use this class directly: instead+-- use 'request'.  Having said that, the 'MkRequest' type defined in this typeclass is important as it determines+-- what values you must pass to 'request' to obtain a proper request.+class HasAjaxRequest endpoint where+    type MkRequest endpoint+    toRequest :: Proxy endpoint -> Request -> MkRequest endpoint++instance (ToJSON a, HasAjaxRequest sub) => HasAjaxRequest (ReqBody '[JSON] a :> sub) where+    type MkRequest (ReqBody '[JSON] a :> sub) = a -> MkRequest sub+    toRequest _ r body = toRequest (Proxy :: Proxy sub) (r+        { rBody = toJSVal_aeson body >>= js_JSONstringify+        , rHeaders = rHeaders r ++ [("Content-Type", "application/json")]+        })++instance (KnownSymbol sym, HasAjaxRequest sub) => HasAjaxRequest (sym :> sub) where+    type MkRequest (sym :> sub) = MkRequest sub+    toRequest _ r = toRequest (Proxy :: Proxy sub) (r { segments = segments r ++ [seg]})+        where+            seg = jsPack $ symbolVal (Proxy :: Proxy sym)++instance (ToHttpApiData v, HasAjaxRequest sub) => HasAjaxRequest (Capture sym v :> sub) where+    type MkRequest (Capture sym v :> sub) = v -> MkRequest sub+    toRequest _ r v = toRequest (Proxy :: Proxy sub) (r { segments = segments r ++ [v'] })+        where+            v' = jsPack $ T.unpack $ toUrlPiece v++instance (KnownSymbol sym, ToHttpApiData a, HasAjaxRequest sub) => HasAjaxRequest (Header sym a :> sub) where+    type MkRequest (Header sym a :> sub) = Maybe a -> MkRequest sub+    toRequest _ r Nothing  = toRequest (Proxy :: Proxy sub) r+    toRequest _ r (Just a) = toRequest (Proxy :: Proxy sub) (r { rHeaders = rHeaders r ++ [(sym',a')]})+        where+            sym' = jsPack $ symbolVal (Proxy :: Proxy sym)+            a' = jsPack $ T.unpack $ toUrlPiece a++instance (KnownSymbol sym, ToHttpApiData a, HasAjaxRequest sub) => HasAjaxRequest (QueryParam sym a :> sub) where+    type MkRequest (QueryParam sym a :> sub) = Maybe a -> MkRequest sub+    toRequest _ r Nothing = toRequest (Proxy :: Proxy sub) r+    toRequest _ r (Just a) = toRequest (Proxy :: Proxy sub) r { rQuery = rQuery r ++ [(sym', a')]}+        where+            sym' = jsPack $ symbolVal (Proxy :: Proxy sym)+            a' = jsPack $ T.unpack $ toUrlPiece a++instance (ReflectMethod m, FromJSON a) => HasAjaxRequest (Verb m s '[JSON] a) where+    type MkRequest (Verb m s '[JSON] a) = HandleResponse a -> IO ()+    toRequest _ r handler = do+        body <- rBody r+        let query :: JSString = case rQuery r of+                        [] -> ""+                        qs -> "?" <> jsIntercalate "&" (map (\(x,y) -> x <> "=" <> y) qs)+        let req = AjaxRequest+                  { reqMethod = textToJSString $ T.decodeUtf8 $ reflectMethod (Proxy :: Proxy m)+                  , reqURI = jsIntercalate "/" (segments r) <> query+                  , reqHeaders = rHeaders r ++ [("Accept", "application/json")]+                  , reqBody = body+                  , reqTimeout = rTimeout r+                  }+        ajax req $ \resp ->+            if respStatus resp == 200+                then do+                    j <- js_JSONParse $ respResponseText resp+                    mv <- fromJSVal j+                    case mv of+                        Nothing -> handler $ Left (500, "Unable to convert response body")+                        Just v -> case fromJSON v of+                            Success v' -> handler $ Right v'+                            Error e -> handler $ Left (500, e)+                else handler $ Left (respStatus resp, jsUnpack $ respResponseText resp)++#ifdef __GHCJS__+foreign import javascript unsafe+    "JSON['parse']($1)"+    js_JSONParse :: JSString -> IO JSVal++foreign import javascript unsafe+    "JSON['stringify']($1)"+    js_JSONstringify :: JSVal -> IO JSVal+#else+js_JSONParse :: JSString -> IO JSVal+js_JSONParse _ = return ()+js_JSONstringify :: JSString -> IO JSVal+js_JSONstringify _ = return ()+#endif