Merge remote-tracking branch 'servant-jquery/prepare-merge' into merge
This commit is contained in:
commit
9da1f6ef40
18 changed files with 1083 additions and 0 deletions
12
servant-jquery/CHANGELOG.md
Normal file
12
servant-jquery/CHANGELOG.md
Normal file
|
@ -0,0 +1,12 @@
|
|||
0.3
|
||||
---
|
||||
* Extend `HeaderArg` to support more advanced HTTP header handling (https://github.com/haskell-servant/servant-jquery/pull/6)
|
||||
* Support content-type aware combinators (but require that endpoints support JSON)
|
||||
* Add support for Matrix params (https://github.com/haskell-servant/servant-jquery/pull/11)
|
||||
* Add functions that directly generate the Javascript code from the API type without having to manually pattern match on the result.
|
||||
|
||||
0.2.2
|
||||
-----
|
||||
|
||||
* Fix an issue where toplevel Raw endpoints would generate a JS function with no name (https://github.com/haskell-servant/servant-jquery/issues/2)
|
||||
* Replace dots by _ in paths (https://github.com/haskell-servant/servant-jquery/issues/1)
|
30
servant-jquery/LICENSE
Normal file
30
servant-jquery/LICENSE
Normal file
|
@ -0,0 +1,30 @@
|
|||
Copyright (c) 2014, Zalora South East Asia Pte Ltd
|
||||
|
||||
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 Zalora South East Asia Pte Ltd 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.
|
97
servant-jquery/README.md
Normal file
97
servant-jquery/README.md
Normal file
|
@ -0,0 +1,97 @@
|
|||
# servant-jquery
|
||||
|
||||
[![Build Status](https://secure.travis-ci.org/haskell-servant/servant-jquery.svg)](http://travis-ci.org/haskell-servant/servant-jquery)
|
||||
[![Coverage Status](https://coveralls.io/repos/haskell-servant/servant-jquery/badge.svg)](https://coveralls.io/r/haskell-servant/servant-jquery)
|
||||
|
||||
![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
|
||||
|
||||
This library lets you derive automatically (JQuery based) Javascript functions that let you query each endpoint of a *servant* webservice.
|
||||
|
||||
## Example
|
||||
|
||||
Read more about the following example [here](https://github.com/haskell-servant/servant-jquery/tree/master/examples#examples).
|
||||
|
||||
``` haskell
|
||||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Aeson
|
||||
import Data.Proxy
|
||||
import GHC.Generics
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
import Servant
|
||||
import Servant.JQuery
|
||||
import System.FilePath
|
||||
|
||||
-- * A simple Counter data type
|
||||
newtype Counter = Counter { value :: Int }
|
||||
deriving (Generic, Show, Num)
|
||||
|
||||
instance ToJSON Counter
|
||||
|
||||
-- * Shared counter operations
|
||||
|
||||
-- Creating a counter that starts from 0
|
||||
newCounter :: IO (TVar Counter)
|
||||
newCounter = newTVarIO 0
|
||||
|
||||
-- Increasing the counter by 1
|
||||
counterPlusOne :: MonadIO m => TVar Counter -> m Counter
|
||||
counterPlusOne counter = liftIO . atomically $ do
|
||||
oldValue <- readTVar counter
|
||||
let newValue = oldValue + 1
|
||||
writeTVar counter newValue
|
||||
return newValue
|
||||
|
||||
currentValue :: MonadIO m => TVar Counter -> m Counter
|
||||
currentValue counter = liftIO $ readTVarIO counter
|
||||
|
||||
-- * Our API type
|
||||
type TestApi = "counter" :> Post Counter -- endpoint for increasing the counter
|
||||
:<|> "counter" :> Get Counter -- endpoint to get the current value
|
||||
:<|> Raw -- used for serving static files
|
||||
|
||||
testApi :: Proxy TestApi
|
||||
testApi = Proxy
|
||||
|
||||
-- * Server-side handler
|
||||
|
||||
-- where our static files reside
|
||||
www :: FilePath
|
||||
www = "examples/www"
|
||||
|
||||
-- defining handlers
|
||||
server :: TVar Counter -> Server TestApi
|
||||
server counter = counterPlusOne counter -- (+1) on the TVar
|
||||
:<|> currentValue counter -- read the TVar
|
||||
:<|> serveDirectory www -- serve static files
|
||||
|
||||
runServer :: TVar Counter -- ^ shared variable for the counter
|
||||
-> Int -- ^ port the server should listen on
|
||||
-> IO ()
|
||||
runServer var port = run port (serve testApi $ server var)
|
||||
|
||||
-- * Generating the JQuery code
|
||||
|
||||
incCounterJS :<|> currentValueJS :<|> _ = jquery testApi
|
||||
|
||||
writeJS :: FilePath -> [AjaxReq] -> IO ()
|
||||
writeJS fp functions = writeFile fp $
|
||||
concatMap generateJS functions
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
-- write the JS code to www/api.js at startup
|
||||
writeJS (www </> "api.js")
|
||||
[ incCounterJS, currentValueJS ]
|
||||
|
||||
-- setup a shared counter
|
||||
cnt <- newCounter
|
||||
|
||||
-- listen to requests on port 8080
|
||||
runServer cnt 8080
|
||||
```
|
2
servant-jquery/Setup.hs
Normal file
2
servant-jquery/Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
|||
import Distribution.Simple
|
||||
main = defaultMain
|
1
servant-jquery/TODO.md
Normal file
1
servant-jquery/TODO.md
Normal file
|
@ -0,0 +1 @@
|
|||
- Investigate the best way to offer cross-origin requests
|
52
servant-jquery/docs.sh
Normal file
52
servant-jquery/docs.sh
Normal file
|
@ -0,0 +1,52 @@
|
|||
SERVANT_DIR=/tmp/servant-jquery-gh-pages
|
||||
|
||||
# Make a temporary clone
|
||||
|
||||
rm -rf $SERVANT_DIR
|
||||
|
||||
git clone . $SERVANT_DIR
|
||||
|
||||
cd $SERVANT_DIR
|
||||
|
||||
# Make sure to pull the latest
|
||||
|
||||
git remote add haskell-servant git@github.com:haskell-servant/servant-jquery.git
|
||||
|
||||
git fetch haskell-servant
|
||||
|
||||
git reset --hard haskell-servant/gh-pages
|
||||
|
||||
# Clear everything away
|
||||
|
||||
git rm -rf $SERVANT_DIR/*
|
||||
|
||||
# Switch back and build the haddocks
|
||||
|
||||
cd -
|
||||
|
||||
cabal configure --builddir=$SERVANT_DIR
|
||||
|
||||
cabal haddock --hoogle --hyperlink-source --html-location='https://hackage.haskell.org/package/$pkg-$version/docs' --builddir=$SERVANT_DIR
|
||||
|
||||
commit_hash=$(git rev-parse HEAD)
|
||||
|
||||
# Move the HTML docs to the root
|
||||
|
||||
cd $SERVANT_DIR
|
||||
|
||||
rm *
|
||||
rm -rf build
|
||||
mv doc/html/servant-jquery/* .
|
||||
rm -r doc/
|
||||
|
||||
# Add everything
|
||||
|
||||
git add .
|
||||
|
||||
git commit -m "Built from $commit_hash"
|
||||
|
||||
# Push to update the pages
|
||||
|
||||
git push haskell-servant HEAD:gh-pages
|
||||
|
||||
rm -rf $SERVANT_DIR
|
17
servant-jquery/examples/README.md
Normal file
17
servant-jquery/examples/README.md
Normal file
|
@ -0,0 +1,17 @@
|
|||
# Examples
|
||||
|
||||
## counter
|
||||
|
||||
This example demonstrates a *servant* server that holds a shared variable (using a `TVar`) and exposes an endpoint for reading its current value and another one for increasing its current value by 1.
|
||||
|
||||
In addition to that, it shows how you can generate the jquery-powered javascript functions corresponding to each endpoint, i.e one for reading the current value and one for increasing the value, and integrates all of that in a very simple HTML page. All these static files are served using the `serveDirectory` function from *servant*.
|
||||
|
||||
To see this all in action, simply run:
|
||||
|
||||
``` bash
|
||||
$ cabal run counter
|
||||
```
|
||||
|
||||
And point your browser to [http://localhost:8080/index.html](http://localhost:8080/index.html).
|
||||
|
||||
Copies of the generated javascript functions and of the generated docs are included in `www/api.js` and `counter.md` respectively.
|
82
servant-jquery/examples/counter.hs
Normal file
82
servant-jquery/examples/counter.hs
Normal file
|
@ -0,0 +1,82 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||
|
||||
import Control.Concurrent.STM
|
||||
import Control.Monad.IO.Class
|
||||
import Data.Aeson
|
||||
import Data.Proxy
|
||||
import GHC.Generics
|
||||
import Network.Wai.Handler.Warp (run)
|
||||
import Servant
|
||||
import Servant.JQuery
|
||||
import System.FilePath
|
||||
|
||||
-- * A simple Counter data type
|
||||
newtype Counter = Counter { value :: Int }
|
||||
deriving (Generic, Show, Num)
|
||||
|
||||
instance ToJSON Counter
|
||||
|
||||
-- * Shared counter operations
|
||||
|
||||
-- Creating a counter that starts from 0
|
||||
newCounter :: IO (TVar Counter)
|
||||
newCounter = newTVarIO 0
|
||||
|
||||
-- Increasing the counter by 1
|
||||
counterPlusOne :: MonadIO m => TVar Counter -> m Counter
|
||||
counterPlusOne counter = liftIO . atomically $ do
|
||||
oldValue <- readTVar counter
|
||||
let newValue = oldValue + 1
|
||||
writeTVar counter newValue
|
||||
return newValue
|
||||
|
||||
currentValue :: MonadIO m => TVar Counter -> m Counter
|
||||
currentValue counter = liftIO $ readTVarIO counter
|
||||
|
||||
-- * Our API type
|
||||
type TestApi = "counter" :> Post Counter -- endpoint for increasing the counter
|
||||
:<|> "counter" :> Get Counter -- endpoint to get the current value
|
||||
:<|> Raw -- used for serving static files
|
||||
|
||||
testApi :: Proxy TestApi
|
||||
testApi = Proxy
|
||||
|
||||
-- * Server-side handler
|
||||
|
||||
-- where our static files reside
|
||||
www :: FilePath
|
||||
www = "examples/www"
|
||||
|
||||
-- defining handlers
|
||||
server :: TVar Counter -> Server TestApi
|
||||
server counter = counterPlusOne counter -- (+1) on the TVar
|
||||
:<|> currentValue counter -- read the TVar
|
||||
:<|> serveDirectory www -- serve static files
|
||||
|
||||
runServer :: TVar Counter -- ^ shared variable for the counter
|
||||
-> Int -- ^ port the server should listen on
|
||||
-> IO ()
|
||||
runServer var port = run port (serve testApi $ server var)
|
||||
|
||||
-- * Generating the JQuery code
|
||||
|
||||
incCounterJS :<|> currentValueJS :<|> _ = jquery testApi
|
||||
|
||||
writeJS :: FilePath -> [AjaxReq] -> IO ()
|
||||
writeJS fp functions = writeFile fp $
|
||||
concatMap generateJS functions
|
||||
|
||||
main :: IO ()
|
||||
main = do
|
||||
-- write the JS code to www/api.js at startup
|
||||
writeJS (www </> "api.js")
|
||||
[ incCounterJS, currentValueJS ]
|
||||
|
||||
-- setup a shared counter
|
||||
cnt <- newCounter
|
||||
|
||||
-- listen to requests on port 8080
|
||||
runServer cnt 8080
|
39
servant-jquery/examples/counter.md
Normal file
39
servant-jquery/examples/counter.md
Normal file
|
@ -0,0 +1,39 @@
|
|||
POST /counter
|
||||
-------------
|
||||
|
||||
**Response**:
|
||||
|
||||
- Status code 201
|
||||
- Response body as below.
|
||||
|
||||
``` javascript
|
||||
{"value":0}
|
||||
```
|
||||
|
||||
GET /doc
|
||||
--------
|
||||
|
||||
**Response**:
|
||||
|
||||
- Status code 200
|
||||
- No response body
|
||||
|
||||
GET /counter
|
||||
------------
|
||||
|
||||
**Response**:
|
||||
|
||||
- Status code 200
|
||||
- Response body as below.
|
||||
|
||||
``` javascript
|
||||
{"value":0}
|
||||
```
|
||||
|
||||
GET /
|
||||
-----
|
||||
|
||||
**Response**:
|
||||
|
||||
- Status code 200
|
||||
- No response body
|
20
servant-jquery/examples/www/api.js
Normal file
20
servant-jquery/examples/www/api.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
|
||||
function postcounter(onSuccess, onError)
|
||||
{
|
||||
$.ajax(
|
||||
{ url: '/counter'
|
||||
, success: onSuccess
|
||||
, error: onError
|
||||
, type: 'POST'
|
||||
});
|
||||
}
|
||||
|
||||
function getcounter(onSuccess, onError)
|
||||
{
|
||||
$.ajax(
|
||||
{ url: '/counter'
|
||||
, success: onSuccess
|
||||
, error: onError
|
||||
, type: 'GET'
|
||||
});
|
||||
}
|
40
servant-jquery/examples/www/index.html
Normal file
40
servant-jquery/examples/www/index.html
Normal file
|
@ -0,0 +1,40 @@
|
|||
<html>
|
||||
<head>
|
||||
<title>Servant: counter</title>
|
||||
<style>
|
||||
body { text-align: center; }
|
||||
#counter { color: green; }
|
||||
#inc { margin: 0px 20px; background-color: green; color: white; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<span id="counter">Counter: 0</span>
|
||||
<button id="inc">Increase</button>
|
||||
or <a href="/doc">view the docs</a>
|
||||
|
||||
<script src="/jquery.min.js" type="text/javascript"></script>
|
||||
<script src="/api.js" type="text/javascript"></script>
|
||||
<script type="text/javascript">
|
||||
$(document).ready(function() {
|
||||
// we get the current value stored by the server when the page is loaded
|
||||
getcounter(updateCounter, alert);
|
||||
|
||||
// we update the value every 1sec, in the same way
|
||||
window.setInterval(function() {
|
||||
getcounter(updateCounter, alert);
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
function updateCounter(response)
|
||||
{
|
||||
$('#counter').html('Counter: ' + response.value);
|
||||
}
|
||||
|
||||
// when the button is clicked, ask the server to increase
|
||||
// the value by one
|
||||
$('#inc').click(function() {
|
||||
postcounter(updateCounter, alert);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
4
servant-jquery/examples/www/jquery.min.js
vendored
Normal file
4
servant-jquery/examples/www/jquery.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
78
servant-jquery/servant-jquery.cabal
Normal file
78
servant-jquery/servant-jquery.cabal
Normal file
|
@ -0,0 +1,78 @@
|
|||
name: servant-jquery
|
||||
version: 0.2.2
|
||||
synopsis: Automatically derive (jquery) javascript functions to query servant webservices
|
||||
description:
|
||||
Automatically derive jquery-based javascript functions to query servant webservices.
|
||||
.
|
||||
Example <https://github.com/haskell-servant/servant-jquery/blob/master/examples/counter.hs here> that serves the generated javascript to a webpage that lets you
|
||||
trigger webservice calls.
|
||||
license: BSD3
|
||||
license-file: LICENSE
|
||||
author: Alp Mestanogullari
|
||||
maintainer: alpmestan@gmail.com
|
||||
copyright: 2014 Alp Mestanogullari
|
||||
category: Web
|
||||
build-type: Simple
|
||||
cabal-version: >=1.10
|
||||
homepage: http://haskell-servant.github.io/
|
||||
Bug-reports: http://github.com/haskell-servant/servant-jquery/issues
|
||||
extra-source-files:
|
||||
CHANGELOG.md
|
||||
README.md
|
||||
source-repository head
|
||||
type: git
|
||||
location: http://github.com/haskell-servant/servant-jquery.git
|
||||
|
||||
flag example
|
||||
description: Build the example too
|
||||
manual: True
|
||||
default: False
|
||||
|
||||
library
|
||||
exposed-modules: Servant.JQuery
|
||||
other-modules: Servant.JQuery.Internal
|
||||
build-depends: base >=4.5 && <5
|
||||
, charset
|
||||
, lens >= 4
|
||||
, servant >= 0.2.2
|
||||
, text
|
||||
hs-source-dirs: src
|
||||
default-language: Haskell2010
|
||||
ghc-options: -Wall
|
||||
|
||||
executable counter
|
||||
main-is: counter.hs
|
||||
ghc-options: -O2 -Wall
|
||||
hs-source-dirs: examples
|
||||
|
||||
if flag(example)
|
||||
buildable: True
|
||||
else
|
||||
buildable: False
|
||||
|
||||
build-depends:
|
||||
aeson
|
||||
, base
|
||||
, filepath
|
||||
, servant >= 0.2.2
|
||||
, servant-server >= 0.2.3
|
||||
, servant-jquery >= 0.2.2
|
||||
, stm
|
||||
, transformers
|
||||
, warp
|
||||
default-language: Haskell2010
|
||||
|
||||
test-suite spec
|
||||
type: exitcode-stdio-1.0
|
||||
hs-source-dirs: test
|
||||
ghc-options: -Wall
|
||||
main-is: Spec.hs
|
||||
build-depends:
|
||||
base == 4.*
|
||||
, lens
|
||||
, servant-jquery
|
||||
, servant
|
||||
, hspec >= 2.0
|
||||
, hspec-expectations
|
||||
, language-ecmascript >= 0.16
|
||||
default-language: Haskell2010
|
117
servant-jquery/src/Servant/JQuery.hs
Normal file
117
servant-jquery/src/Servant/JQuery.hs
Normal file
|
@ -0,0 +1,117 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
-----------------------------------------------------------------------------
|
||||
-- |
|
||||
-- Module : Servant.JQuery
|
||||
-- Copyright : (C) 2014 Alp Mestanogullari
|
||||
-- License : BSD3
|
||||
-- Maintainer : Alp Mestanogullari <alpmestan@gmail.com>
|
||||
-- Stability : experimental
|
||||
-- Portability : non-portable
|
||||
module Servant.JQuery
|
||||
( jquery
|
||||
, generateJS
|
||||
, jsForAPI
|
||||
, printJS
|
||||
, module Servant.JQuery.Internal
|
||||
, GenerateCode(..)
|
||||
) where
|
||||
|
||||
import Control.Lens
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import Data.Proxy
|
||||
import Servant.API
|
||||
import Servant.JQuery.Internal
|
||||
|
||||
jquery :: HasJQ (Canonicalize layout) => Proxy layout -> JQ layout
|
||||
jquery p = jqueryFor (canonicalize p) defReq
|
||||
|
||||
-- js codegen
|
||||
generateJS :: AjaxReq -> String
|
||||
generateJS req = "\n" <>
|
||||
"function " <> fname <> "(" <> argsStr <> ")\n"
|
||||
<> "{\n"
|
||||
<> " $.ajax(\n"
|
||||
<> " { url: " <> url <> "\n"
|
||||
<> " , success: onSuccess\n"
|
||||
<> dataBody
|
||||
<> reqheaders
|
||||
<> " , error: onError\n"
|
||||
<> " , type: '" <> method <> "'\n"
|
||||
<> " });\n"
|
||||
<> "}\n"
|
||||
|
||||
where argsStr = intercalate ", " args
|
||||
args = captures
|
||||
++ map (view argName) queryparams
|
||||
++ body
|
||||
++ map (toValidFunctionName . (<>) "header" . headerArgName) hs
|
||||
++ ["onSuccess", "onError"]
|
||||
|
||||
captures = map captureArg
|
||||
. filter isCapture
|
||||
$ req ^. reqUrl.path
|
||||
|
||||
hs = req ^. reqHeaders
|
||||
|
||||
queryparams = req ^.. reqUrl.queryStr.traverse
|
||||
|
||||
body = if req ^. reqBody
|
||||
then ["body"]
|
||||
else []
|
||||
|
||||
dataBody =
|
||||
if req ^. reqBody
|
||||
then "\n , data: JSON.stringify(body)\n"
|
||||
else ""
|
||||
|
||||
reqheaders =
|
||||
if null hs
|
||||
then ""
|
||||
else "\n , headers: { " ++ headersStr ++ " }\n"
|
||||
|
||||
where headersStr = intercalate ", " $ map headerStr hs
|
||||
headerStr header = "\"" ++
|
||||
headerArgName header ++
|
||||
"\": " ++ show header
|
||||
|
||||
fname = req ^. funcName
|
||||
method = req ^. reqMethod
|
||||
url = if url' == "'" then "'/'" else url'
|
||||
url' = "'"
|
||||
++ urlArgs
|
||||
++ queryArgs
|
||||
|
||||
urlArgs = jsSegments
|
||||
$ req ^.. reqUrl.path.traverse
|
||||
|
||||
queryArgs = if null queryparams
|
||||
then ""
|
||||
else " + '?" ++ jsParams queryparams
|
||||
|
||||
printJS :: AjaxReq -> IO ()
|
||||
printJS = putStrLn . generateJS
|
||||
|
||||
-- | Utility class used by 'jsForAPI' which will
|
||||
-- directly hand you all the Javascript code
|
||||
-- instead of handing you a ':<|>'-separated list
|
||||
-- of 'AjaxReq' like 'jquery' and then having to
|
||||
-- use 'generateJS' on each 'AjaxReq'.
|
||||
class GenerateCode reqs where
|
||||
jsFor :: reqs -> String
|
||||
|
||||
instance GenerateCode AjaxReq where
|
||||
jsFor = generateJS
|
||||
|
||||
instance GenerateCode rest => GenerateCode (AjaxReq :<|> rest) where
|
||||
jsFor (req :<|> rest) = jsFor req ++ jsFor rest
|
||||
|
||||
-- | Directly generate all the javascript functions for your API
|
||||
-- from a 'Proxy' for your API type. You can then write it to
|
||||
-- a file or integrate it in a page, for example.
|
||||
jsForAPI :: (HasJQ (Canonicalize api), GenerateCode (JQ api))
|
||||
=> Proxy api -> String
|
||||
jsForAPI p = jsFor (jquery p)
|
339
servant-jquery/src/Servant/JQuery/Internal.hs
Normal file
339
servant-jquery/src/Servant/JQuery/Internal.hs
Normal file
|
@ -0,0 +1,339 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# LANGUAGE ConstraintKinds #-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE UndecidableInstances #-}
|
||||
module Servant.JQuery.Internal where
|
||||
|
||||
import Control.Applicative
|
||||
import Control.Lens
|
||||
import Data.Char (toLower)
|
||||
import qualified Data.CharSet as Set
|
||||
import qualified Data.CharSet.Unicode.Category as Set
|
||||
import Data.List
|
||||
import Data.Monoid
|
||||
import Data.Proxy
|
||||
import qualified Data.Text as T
|
||||
import GHC.Exts (Constraint)
|
||||
import GHC.TypeLits
|
||||
import Servant.API
|
||||
|
||||
type Arg = String
|
||||
|
||||
data Segment = Segment { _segment :: SegmentType, _matrix :: [MatrixArg] }
|
||||
deriving (Eq, Show)
|
||||
|
||||
data SegmentType = Static String -- ^ a static path segment. like "/foo"
|
||||
| Cap Arg -- ^ a capture. like "/:userid"
|
||||
deriving (Eq, Show)
|
||||
|
||||
type Path = [Segment]
|
||||
|
||||
data ArgType =
|
||||
Normal
|
||||
| Flag
|
||||
| List
|
||||
deriving (Eq, Show)
|
||||
|
||||
data QueryArg = QueryArg
|
||||
{ _argName :: Arg
|
||||
, _argType :: ArgType
|
||||
} deriving (Eq, Show)
|
||||
|
||||
data HeaderArg = HeaderArg
|
||||
{ headerArgName :: String
|
||||
}
|
||||
| ReplaceHeaderArg
|
||||
{ headerArgName :: String
|
||||
, headerPattern :: String
|
||||
} deriving (Eq)
|
||||
|
||||
instance Show HeaderArg where
|
||||
show (HeaderArg n) = toValidFunctionName ("header" <> n)
|
||||
show (ReplaceHeaderArg n p)
|
||||
| pn `isPrefixOf` p = pv <> " + \"" <> rp <> "\""
|
||||
| pn `isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
|
||||
| pn `isInfixOf` p = "\"" <> (replace pn ("\" + " <> pv <> " + \"") p)
|
||||
<> "\""
|
||||
| otherwise = p
|
||||
where
|
||||
pv = toValidFunctionName ("header" <> n)
|
||||
pn = "{" <> n <> "}"
|
||||
rp = replace pn "" p
|
||||
-- Use replace method from Data.Text
|
||||
replace old new = T.unpack .
|
||||
T.replace (T.pack old) (T.pack new) .
|
||||
T.pack
|
||||
|
||||
-- | Attempts to reduce the function name provided to that allowed by JS.
|
||||
-- https://mathiasbynens.be/notes/javascript-identifiers
|
||||
-- Couldn't work out how to handle zero-width characters.
|
||||
-- @TODO: specify better default function name, or throw error?
|
||||
toValidFunctionName :: String -> String
|
||||
toValidFunctionName (x:xs) = [setFirstChar x] <> filter remainder xs
|
||||
where
|
||||
setFirstChar c = if firstChar c
|
||||
then c
|
||||
else '_'
|
||||
firstChar c = (prefixOK c) || (or . map (Set.member c) $ firstLetterOK)
|
||||
remainder c = (prefixOK c) || (or . map (Set.member c) $ remainderOK)
|
||||
-- Valid prefixes
|
||||
prefixOK c = c `elem` ['$','_']
|
||||
-- Unicode character sets
|
||||
firstLetterOK = [ Set.lowercaseLetter
|
||||
, Set.uppercaseLetter
|
||||
, Set.titlecaseLetter
|
||||
, Set.modifierLetter
|
||||
, Set.otherLetter
|
||||
, Set.letterNumber ]
|
||||
remainderOK = firstLetterOK <> [ Set.nonSpacingMark
|
||||
, Set.spacingCombiningMark
|
||||
, Set.decimalNumber
|
||||
, Set.connectorPunctuation ]
|
||||
toValidFunctionName [] = "_"
|
||||
|
||||
type MatrixArg = QueryArg
|
||||
|
||||
data Url = Url
|
||||
{ _path :: Path
|
||||
, _queryStr :: [QueryArg]
|
||||
} deriving (Eq, Show)
|
||||
|
||||
defUrl :: Url
|
||||
defUrl = Url [] []
|
||||
|
||||
type FunctionName = String
|
||||
type Method = String
|
||||
|
||||
data AjaxReq = AjaxReq
|
||||
{ _reqUrl :: Url
|
||||
, _reqMethod :: Method
|
||||
, _reqHeaders :: [HeaderArg]
|
||||
, _reqBody :: Bool
|
||||
, _funcName :: FunctionName
|
||||
} deriving (Eq, Show)
|
||||
|
||||
makeLenses ''QueryArg
|
||||
makeLenses ''Segment
|
||||
makeLenses ''Url
|
||||
makeLenses ''AjaxReq
|
||||
|
||||
isCapture :: Segment -> Bool
|
||||
isCapture (Segment (Cap _) _) = True
|
||||
isCapture _ = False
|
||||
|
||||
hasMatrixArgs :: Segment -> Bool
|
||||
hasMatrixArgs (Segment _ (_:_)) = True
|
||||
hasMatrixArgs _ = False
|
||||
|
||||
hasArgs :: Segment -> Bool
|
||||
hasArgs s = isCapture s || hasMatrixArgs s
|
||||
|
||||
matrixArgs :: Segment -> [MatrixArg]
|
||||
matrixArgs (Segment _ ms) = ms
|
||||
|
||||
captureArg :: Segment -> Arg
|
||||
captureArg (Segment (Cap s) _) = s
|
||||
captureArg _ = error "captureArg called on non capture"
|
||||
|
||||
jsSegments :: [Segment] -> String
|
||||
jsSegments [] = ""
|
||||
jsSegments [x] = "/" ++ segmentToStr x False
|
||||
jsSegments (x:xs) = "/" ++ segmentToStr x True ++ jsSegments xs
|
||||
|
||||
segmentToStr :: Segment -> Bool -> String
|
||||
segmentToStr (Segment st ms) notTheEnd =
|
||||
segmentTypeToStr st ++ jsMParams ms ++ if notTheEnd then "" else "'"
|
||||
|
||||
segmentTypeToStr :: SegmentType -> String
|
||||
segmentTypeToStr (Static s) = s
|
||||
segmentTypeToStr (Cap s) = "' + encodeURIComponent(" ++ s ++ ") + '"
|
||||
|
||||
jsGParams :: String -> [QueryArg] -> String
|
||||
jsGParams _ [] = ""
|
||||
jsGParams _ [x] = paramToStr x False
|
||||
jsGParams s (x:xs) = paramToStr x True ++ s ++ jsGParams s xs
|
||||
|
||||
jsParams :: [QueryArg] -> String
|
||||
jsParams = jsGParams "&"
|
||||
|
||||
jsMParams :: [MatrixArg] -> String
|
||||
jsMParams [] = ""
|
||||
jsMParams xs = ";" ++ jsGParams ";" xs
|
||||
|
||||
paramToStr :: QueryArg -> Bool -> String
|
||||
paramToStr qarg notTheEnd =
|
||||
case qarg ^. argType of
|
||||
Normal -> name
|
||||
++ "=' + encodeURIComponent("
|
||||
++ name
|
||||
++ if notTheEnd then ") + '" else ")"
|
||||
|
||||
Flag -> name ++ "="
|
||||
|
||||
List -> name
|
||||
++ "[]=' + encodeURIComponent("
|
||||
++ name
|
||||
++ if notTheEnd then ") + '" else ")"
|
||||
|
||||
where name = qarg ^. argName
|
||||
|
||||
defReq :: AjaxReq
|
||||
defReq = AjaxReq defUrl "GET" [] False ""
|
||||
|
||||
type family Elem (a :: *) (ls::[*]) :: Constraint where
|
||||
Elem a '[] = 'False ~ 'True
|
||||
Elem a (a ': list) = ()
|
||||
Elem a (b ': list) = Elem a list
|
||||
|
||||
class HasJQ (layout :: *) where
|
||||
type JQ' layout :: *
|
||||
jqueryFor :: Proxy layout -> AjaxReq -> JQ' layout
|
||||
|
||||
type JQ layout = JQ' (Canonicalize layout)
|
||||
|
||||
instance (HasJQ a, HasJQ b)
|
||||
=> HasJQ (a :<|> b) where
|
||||
type JQ' (a :<|> b) = JQ' a :<|> JQ' b
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy a) req
|
||||
:<|> jqueryFor (Proxy :: Proxy b) req
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (Capture sym a :> sublayout) where
|
||||
type JQ' (Capture sym a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.path <>~ [Segment (Cap str) []]
|
||||
|
||||
where str = symbolVal (Proxy :: Proxy sym)
|
||||
|
||||
instance HasJQ Delete where
|
||||
type JQ' Delete = AjaxReq
|
||||
|
||||
jqueryFor Proxy req =
|
||||
req & funcName %~ ("delete" <>)
|
||||
& reqMethod .~ "DELETE"
|
||||
|
||||
instance Elem JSON list => HasJQ (Get list a) where
|
||||
type JQ' (Get list a) = AjaxReq
|
||||
|
||||
jqueryFor Proxy req =
|
||||
req & funcName %~ ("get" <>)
|
||||
& reqMethod .~ "GET"
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (Header sym a :> sublayout) where
|
||||
type JQ' (Header sym a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor subP (req & reqHeaders <>~ [HeaderArg hname])
|
||||
|
||||
where hname = symbolVal (Proxy :: Proxy sym)
|
||||
subP = Proxy :: Proxy sublayout
|
||||
|
||||
instance Elem JSON list => HasJQ (Post list a) where
|
||||
type JQ' (Post list a) = AjaxReq
|
||||
|
||||
jqueryFor Proxy req =
|
||||
req & funcName %~ ("post" <>)
|
||||
& reqMethod .~ "POST"
|
||||
|
||||
instance Elem JSON list => HasJQ (Put list a) where
|
||||
type JQ' (Put list a) = AjaxReq
|
||||
|
||||
jqueryFor Proxy req =
|
||||
req & funcName %~ ("put" <>)
|
||||
& reqMethod .~ "PUT"
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (QueryParam sym a :> sublayout) where
|
||||
type JQ' (QueryParam sym a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.queryStr <>~ [QueryArg str Normal]
|
||||
|
||||
where str = symbolVal (Proxy :: Proxy sym)
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (QueryParams sym a :> sublayout) where
|
||||
type JQ' (QueryParams sym a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.queryStr <>~ [QueryArg str List]
|
||||
|
||||
where str = symbolVal (Proxy :: Proxy sym)
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (QueryFlag sym :> sublayout) where
|
||||
type JQ' (QueryFlag sym :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.queryStr <>~ [QueryArg str Flag]
|
||||
|
||||
where str = symbolVal (Proxy :: Proxy sym)
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (MatrixParam sym a :> sublayout) where
|
||||
type JQ' (MatrixParam sym a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.path._last.matrix <>~ [QueryArg strArg Normal]
|
||||
|
||||
where str = symbolVal (Proxy :: Proxy sym)
|
||||
strArg = str ++ "Value"
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (MatrixParams sym a :> sublayout) where
|
||||
type JQ' (MatrixParams sym a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.path._last.matrix <>~ [QueryArg str List]
|
||||
|
||||
where str = symbolVal (Proxy :: Proxy sym)
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (MatrixFlag sym :> sublayout) where
|
||||
type JQ' (MatrixFlag sym :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.path._last.matrix <>~ [QueryArg str Flag]
|
||||
|
||||
where str = symbolVal (Proxy :: Proxy sym)
|
||||
|
||||
instance HasJQ Raw where
|
||||
type JQ' Raw = Method -> AjaxReq
|
||||
|
||||
jqueryFor Proxy req method =
|
||||
req & funcName %~ ((toLower <$> method) <>)
|
||||
& reqMethod .~ method
|
||||
|
||||
instance (Elem JSON list, HasJQ sublayout) => HasJQ (ReqBody list a :> sublayout) where
|
||||
type JQ' (ReqBody list a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqBody .~ True
|
||||
|
||||
instance (KnownSymbol path, HasJQ sublayout)
|
||||
=> HasJQ (path :> sublayout) where
|
||||
type JQ' (path :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req =
|
||||
jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqUrl.path <>~ [Segment (Static str) []]
|
||||
& funcName %~ (str <>)
|
||||
|
||||
where str = map (\c -> if c == '.' then '_' else c) $ symbolVal (Proxy :: Proxy path)
|
96
servant-jquery/test/Servant/JQuerySpec.hs
Normal file
96
servant-jquery/test/Servant/JQuerySpec.hs
Normal file
|
@ -0,0 +1,96 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE QuasiQuotes #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||
module Servant.JQuerySpec where
|
||||
|
||||
import Data.Either (isRight)
|
||||
import Data.Proxy
|
||||
import Language.ECMAScript3.Parser (parseFromString)
|
||||
import Test.Hspec
|
||||
|
||||
import Servant.API
|
||||
import Servant.JQuery
|
||||
import Servant.JQuerySpec.CustomHeaders
|
||||
|
||||
type TestAPI = "simple" :> ReqBody '[JSON,FormUrlEncoded] String :> Post '[JSON] Bool
|
||||
:<|> "has.extension" :> Get '[FormUrlEncoded,JSON] Bool
|
||||
|
||||
type TopLevelRawAPI = "something" :> Get '[JSON] Int
|
||||
:<|> Raw
|
||||
|
||||
type HeaderHandlingAPI = "something" :> Header "Foo" String
|
||||
:> Get '[JSON] Int
|
||||
|
||||
type CustomAuthAPI = "something" :> Authorization "Basic" String
|
||||
:> Get '[JSON] Int
|
||||
|
||||
type CustomHeaderAPI = "something" :> MyLovelyHorse String
|
||||
:> Get '[JSON] Int
|
||||
|
||||
type CustomHeaderAPI2 = "something" :> WhatsForDinner String
|
||||
:> Get '[JSON] Int
|
||||
|
||||
headerHandlingProxy :: Proxy HeaderHandlingAPI
|
||||
headerHandlingProxy = Proxy
|
||||
|
||||
customAuthProxy :: Proxy CustomAuthAPI
|
||||
customAuthProxy = Proxy
|
||||
|
||||
customHeaderProxy :: Proxy CustomHeaderAPI
|
||||
customHeaderProxy = Proxy
|
||||
|
||||
customHeaderProxy2 :: Proxy CustomHeaderAPI2
|
||||
customHeaderProxy2 = Proxy
|
||||
|
||||
spec :: Spec
|
||||
spec = describe "Servant.JQuery"
|
||||
generateJSSpec
|
||||
|
||||
generateJSSpec :: Spec
|
||||
generateJSSpec = describe "generateJS" $ do
|
||||
it "should generate valid javascript" $ do
|
||||
let (postSimple :<|> getHasExtension ) = jquery (Proxy :: Proxy TestAPI)
|
||||
parseFromString (generateJS postSimple) `shouldSatisfy` isRight
|
||||
parseFromString (generateJS getHasExtension) `shouldSatisfy` isRight
|
||||
print $ generateJS getHasExtension
|
||||
|
||||
it "should use non-empty function names" $ do
|
||||
let (_ :<|> topLevel) = jquery (Proxy :: Proxy TopLevelRawAPI)
|
||||
print $ generateJS $ topLevel "GET"
|
||||
parseFromString (generateJS $ topLevel "GET") `shouldSatisfy` isRight
|
||||
|
||||
it "should handle simple HTTP headers" $ do
|
||||
let jsText = generateJS $ jquery headerHandlingProxy
|
||||
print jsText
|
||||
parseFromString jsText `shouldSatisfy` isRight
|
||||
jsText `shouldContain` "headerFoo"
|
||||
jsText `shouldContain` "headers: { \"Foo\": headerFoo }\n"
|
||||
|
||||
it "should handle complex HTTP headers" $ do
|
||||
let jsText = generateJS $ jquery customAuthProxy
|
||||
print jsText
|
||||
parseFromString jsText `shouldSatisfy` isRight
|
||||
jsText `shouldContain` "headerAuthorization"
|
||||
jsText `shouldContain` "headers: { \"Authorization\": \"Basic \" + headerAuthorization }\n"
|
||||
|
||||
it "should handle complex, custom HTTP headers" $ do
|
||||
let jsText = generateJS $ jquery customHeaderProxy
|
||||
print jsText
|
||||
parseFromString jsText `shouldSatisfy` isRight
|
||||
jsText `shouldContain` "headerXMyLovelyHorse"
|
||||
jsText `shouldContain` "headers: { \"X-MyLovelyHorse\": \"I am good friends with \" + headerXMyLovelyHorse }\n"
|
||||
|
||||
it "should handle complex, custom HTTP headers (template replacement)" $ do
|
||||
let jsText = generateJS $ jquery customHeaderProxy2
|
||||
print jsText
|
||||
parseFromString jsText `shouldSatisfy` isRight
|
||||
jsText `shouldContain` "headerXWhatsForDinner"
|
||||
jsText `shouldContain` "headers: { \"X-WhatsForDinner\": \"I would like \" + headerXWhatsForDinner + \" with a cherry on top.\" }\n"
|
||||
|
||||
it "can generate the whole javascript code string at once with jsForAPI" $ do
|
||||
let jsStr = jsForAPI (Proxy :: Proxy TestAPI)
|
||||
parseFromString jsStr `shouldSatisfy` isRight
|
55
servant-jquery/test/Servant/JQuerySpec/CustomHeaders.hs
Normal file
55
servant-jquery/test/Servant/JQuerySpec/CustomHeaders.hs
Normal file
|
@ -0,0 +1,55 @@
|
|||
{-# LANGUAGE DataKinds #-}
|
||||
{-# LANGUAGE FlexibleInstances #-}
|
||||
{-# LANGUAGE KindSignatures #-}
|
||||
{-# LANGUAGE PolyKinds #-}
|
||||
{-# LANGUAGE ScopedTypeVariables #-}
|
||||
{-# LANGUAGE TypeFamilies #-}
|
||||
{-# LANGUAGE TypeOperators #-}
|
||||
|
||||
module Servant.JQuerySpec.CustomHeaders where
|
||||
|
||||
import Control.Lens
|
||||
import Data.Monoid
|
||||
import Data.Proxy
|
||||
import GHC.TypeLits
|
||||
import Servant.API
|
||||
import Servant.JQuery
|
||||
|
||||
-- | This is a hypothetical combinator that fetches an Authorization header.
|
||||
-- The symbol in the header denotes what kind of authentication we are
|
||||
-- using -- Basic, Digest, whatever.
|
||||
data Authorization (sym :: Symbol) a
|
||||
|
||||
instance (KnownSymbol sym, HasJQ sublayout)
|
||||
=> HasJQ (Authorization sym a :> sublayout) where
|
||||
type JQ' (Authorization sym a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req = jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqHeaders <>~ [ ReplaceHeaderArg "Authorization" $
|
||||
tokenType (symbolVal (Proxy :: Proxy sym)) ]
|
||||
where
|
||||
tokenType t = t <> " {Authorization}"
|
||||
|
||||
-- | This is a combinator that fetches an X-MyLovelyHorse header.
|
||||
data MyLovelyHorse a
|
||||
|
||||
instance (HasJQ sublayout)
|
||||
=> HasJQ (MyLovelyHorse a :> sublayout) where
|
||||
type JQ' (MyLovelyHorse a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req = jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqHeaders <>~ [ ReplaceHeaderArg "X-MyLovelyHorse" tpl ]
|
||||
where
|
||||
tpl = "I am good friends with {X-MyLovelyHorse}"
|
||||
|
||||
-- | This is a combinator that fetches an X-WhatsForDinner header.
|
||||
data WhatsForDinner a
|
||||
|
||||
instance (HasJQ sublayout)
|
||||
=> HasJQ (WhatsForDinner a :> sublayout) where
|
||||
type JQ' (WhatsForDinner a :> sublayout) = JQ' sublayout
|
||||
|
||||
jqueryFor Proxy req = jqueryFor (Proxy :: Proxy sublayout) $
|
||||
req & reqHeaders <>~ [ ReplaceHeaderArg "X-WhatsForDinner" tpl ]
|
||||
where
|
||||
tpl = "I would like {X-WhatsForDinner} with a cherry on top."
|
2
servant-jquery/test/Spec.hs
Normal file
2
servant-jquery/test/Spec.hs
Normal file
|
@ -0,0 +1,2 @@
|
|||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
||||
|
Loading…
Reference in a new issue