This commit is contained in:
parent
fdd1829c8f
commit
589f8757f9
9 changed files with 584 additions and 0 deletions
14
servant-purescript/.gitignore
vendored
Normal file
14
servant-purescript/.gitignore
vendored
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
dist/
|
||||||
|
cabal.sandbox.config
|
||||||
|
.cabal-sandbox
|
||||||
|
*/**/*.swp
|
||||||
|
*/**/*.swo
|
||||||
|
examples/*/bower_components/**/*
|
||||||
|
examples/*/output/**/*
|
||||||
|
|
||||||
|
# Auto-generated
|
||||||
|
examples/counter/temp/api.purs
|
||||||
|
examples/counter/www/api.js
|
||||||
|
examples/todo/src/App/App.Ajax.purs
|
||||||
|
examples/todo/www/app.js
|
||||||
|
|
13
servant-purescript/.travis.yml
Normal file
13
servant-purescript/.travis.yml
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
language: haskell
|
||||||
|
ghc:
|
||||||
|
- 7.8
|
||||||
|
notifications:
|
||||||
|
email:
|
||||||
|
on_success: change
|
||||||
|
on_failure: change
|
||||||
|
before_install:
|
||||||
|
- cabal sandbox init
|
||||||
|
- git clone https://github.com/haskell-servant/servant.git
|
||||||
|
- cabal sandbox add-source servant/
|
||||||
|
- git clone https://github.com/haskell-servant/servant-jquery.git
|
||||||
|
- cabal sandbox add-source servant-jquery/
|
9
servant-purescript/HLint.hs
Normal file
9
servant-purescript/HLint.hs
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
{-# LANGUAGE PackageImports #-}
|
||||||
|
module HLint.HLint where
|
||||||
|
|
||||||
|
import "hint" HLint.Builtin.All
|
||||||
|
import "hint" HLint.Default
|
||||||
|
import "hint" HLint.Dollar
|
||||||
|
import "hint" HLint.Generalise
|
||||||
|
|
||||||
|
ignore "Redundant bracket" = Domains.Rest.Client
|
30
servant-purescript/LICENSE
Normal file
30
servant-purescript/LICENSE
Normal file
|
@ -0,0 +1,30 @@
|
||||||
|
Copyright (c) 2015, Geoffrey Roberts
|
||||||
|
|
||||||
|
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 Geoffrey Roberts 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.
|
164
servant-purescript/README.md
Normal file
164
servant-purescript/README.md
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
# servant-purescript
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/anchor/servant-purescript.svg?branch=master)](https://travis-ci.org/anchor/servant-purescript)
|
||||||
|
|
||||||
|
This library lets you automatically derive Purescript functions (using jQuery's AJAX capabilities) that let you query each endpoint of a [*servant*](http://haskell-servant.github.io) webservice.
|
||||||
|
|
||||||
|
© Anchor 2015
|
||||||
|
|
||||||
|
## Purescript dependencies
|
||||||
|
|
||||||
|
Projects that use *servant-purescript* will depend on the following Purescript libraries:
|
||||||
|
|
||||||
|
* [purescript-affjax](https://github.com/slamdata/purescript-affjax)
|
||||||
|
* [purescript-arrays](https://github.com/purescript/purescript-arrays)
|
||||||
|
* [purescript-control](https://github.com/purescript/purescript-control)
|
||||||
|
* [purescript-either](https://github.com/purescript/purescript-either)
|
||||||
|
* [purescript-foldable-traversable](https://github.com/purescript/purescript-foldable-traversable)
|
||||||
|
* [purescript-foreign](https://github.com/purescript/purescript-foreign)
|
||||||
|
* [purescript-maybe](https://github.com/purescript/purescript-maybe)
|
||||||
|
* [purescript-monoid](https://github.com/purescript/purescript-monoid)
|
||||||
|
* [purescript-tuples](https://github.com/purescript/purescript-tuples)
|
||||||
|
|
||||||
|
You should be able to get all these libraries if you require `purescript-foreign` on its own.
|
||||||
|
|
||||||
|
It's recommended that you use [bower](http://bower.io) to manage dependencies for your project. Use the bower.json file in examples as a reference.
|
||||||
|
|
||||||
|
## Usage example
|
||||||
|
|
||||||
|
The following example is ported from the example provided with [*servant-jquery*](https://github.com/haskell-servant/servant-jquery).
|
||||||
|
|
||||||
|
To build it (and the tests), run the following:
|
||||||
|
|
||||||
|
`cabal configure --enable-tests --flags="example"`
|
||||||
|
|
||||||
|
And to run:
|
||||||
|
|
||||||
|
`cabal run counter`
|
||||||
|
|
||||||
|
Here's [more information about the original example](https://github.com/haskell-servant/servant-jquery/tree/master/examples#examples) that will explain how it works and how it should run.
|
||||||
|
|
||||||
|
The PureScript version is a direct port of this. The *servant-purescript* bindings are invoked within Javascript.
|
||||||
|
|
||||||
|
```haskell
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
|
||||||
|
|
||||||
|
import Control.Concurrent.STM
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.List
|
||||||
|
import Data.Monoid
|
||||||
|
import Data.Proxy
|
||||||
|
import GHC.Generics
|
||||||
|
import Network.Wai.Handler.Warp (run)
|
||||||
|
import Servant
|
||||||
|
import Servant.JQuery
|
||||||
|
import Servant.PureScript
|
||||||
|
import System.FilePath
|
||||||
|
import System.FilePath.Glob
|
||||||
|
import System.Process
|
||||||
|
|
||||||
|
-- * 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"
|
||||||
|
|
||||||
|
-- where temporary files reside
|
||||||
|
tmp :: FilePath
|
||||||
|
tmp = "examples/temp"
|
||||||
|
|
||||||
|
-- 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
|
||||||
|
|
||||||
|
writePS :: FilePath -> [AjaxReq] -> IO ()
|
||||||
|
writePS fp functions = writeFile fp $
|
||||||
|
generatePSModule defaultSettings "App" functions
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = do
|
||||||
|
-- Write the PureScript module
|
||||||
|
writePS (tmp </> "api.purs") [ incCounterJS
|
||||||
|
, currentValueJS
|
||||||
|
]
|
||||||
|
|
||||||
|
-- Run bower to import dependencies
|
||||||
|
_ <- system "cd examples && bower install"
|
||||||
|
|
||||||
|
(matches, _) <- globDir [compile "examples/bower_components/**/*.purs"] "."
|
||||||
|
|
||||||
|
-- Compile PureScript to JS
|
||||||
|
let cmd = "psc "
|
||||||
|
<> (intercalate " " $ head matches)
|
||||||
|
<> " "
|
||||||
|
<> (tmp </> "api.purs")
|
||||||
|
<> " > "
|
||||||
|
<> (www </> "api.js")
|
||||||
|
|
||||||
|
putStrLn cmd
|
||||||
|
|
||||||
|
_ <- system cmd
|
||||||
|
|
||||||
|
-- setup a shared counter
|
||||||
|
cnt <- newCounter
|
||||||
|
|
||||||
|
-- listen to requests on port 8080
|
||||||
|
runServer cnt 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
## Another example
|
||||||
|
|
||||||
|
There is a simple todo list example that can be built using this command:
|
||||||
|
|
||||||
|
`cabal configure --enable-tests --flags="example"`
|
||||||
|
|
||||||
|
The example can then be run using this command:
|
||||||
|
|
||||||
|
`cabal run todolist`
|
2
servant-purescript/Setup.hs
Normal file
2
servant-purescript/Setup.hs
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
import Distribution.Simple
|
||||||
|
main = defaultMain
|
57
servant-purescript/servant-purescript.cabal
Normal file
57
servant-purescript/servant-purescript.cabal
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
name: servant-purescript
|
||||||
|
version: 0.1
|
||||||
|
synopsis: Automatically derive purescript functions to query servant webservices
|
||||||
|
description:
|
||||||
|
Automatically derive purescript functions to query servant webservices.
|
||||||
|
license: BSD3
|
||||||
|
license-file: LICENSE
|
||||||
|
author: Anchor Engineering <engineering@anchor.com.au>
|
||||||
|
maintainer: Anchor Engineering <engineering@anchor.com.au>
|
||||||
|
copyright: 2015 Anchor
|
||||||
|
category: Web
|
||||||
|
build-type: Simple
|
||||||
|
cabal-version: >=1.10
|
||||||
|
Bug-reports: http://github.com/anchor/servant-purescript/issues
|
||||||
|
source-repository head
|
||||||
|
type: git
|
||||||
|
location: http://github.com/anchor/servant-purescript.git
|
||||||
|
|
||||||
|
flag example
|
||||||
|
description: Build the example too
|
||||||
|
manual: True
|
||||||
|
default: False
|
||||||
|
|
||||||
|
library
|
||||||
|
exposed-modules: Servant.PureScript
|
||||||
|
build-depends: base >=4.8.1.0 && <5
|
||||||
|
, lens >=4
|
||||||
|
, purescript >=0.7.5.3
|
||||||
|
, servant >=0.5
|
||||||
|
, servant-foreign >=0.5
|
||||||
|
, servant-js >=0.5
|
||||||
|
, string-conversions
|
||||||
|
, text
|
||||||
|
hs-source-dirs: src
|
||||||
|
default-language: Haskell2010
|
||||||
|
ghc-options: -Wall
|
||||||
|
|
||||||
|
test-suite spec
|
||||||
|
type: exitcode-stdio-1.0
|
||||||
|
hs-source-dirs: test
|
||||||
|
ghc-options: -Wall
|
||||||
|
main-is: Spec.hs
|
||||||
|
build-depends:
|
||||||
|
base >=4.8.1.0 && <5
|
||||||
|
, hspec >=2.0
|
||||||
|
, hspec-expectations
|
||||||
|
, language-ecmascript >=0.17
|
||||||
|
, lens >= 4
|
||||||
|
, parsec
|
||||||
|
, process
|
||||||
|
, purescript >=0.7.5.3
|
||||||
|
, servant >=0.5
|
||||||
|
, servant-foreign >=0.5
|
||||||
|
, servant-js >=0.5
|
||||||
|
, servant-purescript ==0.0.2
|
||||||
|
, string-conversions
|
||||||
|
default-language: Haskell2010
|
230
servant-purescript/src/Servant/PureScript.hs
Normal file
230
servant-purescript/src/Servant/PureScript.hs
Normal file
|
@ -0,0 +1,230 @@
|
||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
|
|
||||||
|
module Servant.PureScript (
|
||||||
|
generatePSModule,
|
||||||
|
generatePSUtilModule,
|
||||||
|
generatePS,
|
||||||
|
PSSettings(..),
|
||||||
|
baseURL,
|
||||||
|
defaultSettings
|
||||||
|
) where
|
||||||
|
|
||||||
|
import Control.Arrow ((&&&))
|
||||||
|
import Control.Lens (makeLenses, (^.), (^..), view)
|
||||||
|
import Data.Char (toUpper, toLower)
|
||||||
|
import Data.Proxy (Proxy(Proxy))
|
||||||
|
import Data.String.Conversions (ConvertibleStrings, ST, cs, (<>))
|
||||||
|
import qualified Data.Text as T
|
||||||
|
import qualified Servant.JS as JS
|
||||||
|
import qualified Servant.JS.Internal as JS
|
||||||
|
import qualified Servant.Foreign as F
|
||||||
|
|
||||||
|
|
||||||
|
-- | PureScript rendering settings
|
||||||
|
data PSSettings = PSSettings {
|
||||||
|
_baseURL :: String, -- ^ Base URL for AJAX requests
|
||||||
|
_utilModuleName :: ST -- ^ module that all generated ajax modules depend on
|
||||||
|
}
|
||||||
|
|
||||||
|
makeLenses ''PSSettings
|
||||||
|
|
||||||
|
-- | (may be obsoleted by https://github.com/purescript/purescript-globals/pull/7 at some point.)
|
||||||
|
generatePSUtilModule :: PSSettings -> (ST, ST)
|
||||||
|
generatePSUtilModule settings = (purs, js)
|
||||||
|
where
|
||||||
|
purs = T.unlines
|
||||||
|
[ "module " <> (settings ^. utilModuleName) <> " where"
|
||||||
|
, "foreign import encodeURIComponent :: String -> String"
|
||||||
|
]
|
||||||
|
js = T.unlines
|
||||||
|
[ "\"use strict\";"
|
||||||
|
, "// module " <> (settings ^. utilModuleName)
|
||||||
|
, "exports.encodeURIComponent = encodeURIComponent;"
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Given a servant api, generate a PureScript module containing a list of functions for AJAX
|
||||||
|
-- requests.
|
||||||
|
generatePSModule :: (JS.GenerateList (F.Foreign api), F.HasForeign api) => PSSettings -> ST -> Proxy api -> ST
|
||||||
|
generatePSModule settings mname proxy =
|
||||||
|
generatePSModule' settings mname $ JS.generateList (F.foreignFor proxy F.defReq)
|
||||||
|
|
||||||
|
-- | Given a list of foreign requests, generate a PureScript module containing a list of functions
|
||||||
|
-- for AJAX requests.
|
||||||
|
generatePSModule'
|
||||||
|
:: PSSettings -- ^ PureScript rendering settings
|
||||||
|
-> ST -- ^ Name of PureScript module
|
||||||
|
-> [F.Req] -- ^ List of AJAX requests to render in module
|
||||||
|
-> ST -- ^ Rendered PureScript module
|
||||||
|
generatePSModule' settings mname reqs = T.unlines $
|
||||||
|
[ "module " <> mname <> " where"
|
||||||
|
, ""
|
||||||
|
, "import Prelude"
|
||||||
|
, "import Data.Foreign"
|
||||||
|
, "import Data.Maybe"
|
||||||
|
, "import Network.HTTP.Affjax"
|
||||||
|
, "import Network.HTTP.Method"
|
||||||
|
, "import Network.HTTP.RequestHeader"
|
||||||
|
, "import " <> (settings ^. utilModuleName) <> " (encodeURIComponent)"
|
||||||
|
, ""
|
||||||
|
, T.intercalate "\n" (generatePS settings <$> reqs)
|
||||||
|
]
|
||||||
|
|
||||||
|
-- | Generate a single PureScript function for an AJAX request.
|
||||||
|
-- To prevent conflicts, generates a unique function name for every available
|
||||||
|
-- function name and set of captures.
|
||||||
|
generatePS
|
||||||
|
:: PSSettings -- ^ PureScript rendering settings
|
||||||
|
-> F.Req -- ^ AJAX request to render
|
||||||
|
-> ST -- ^ Rendered PureScript
|
||||||
|
generatePS settings req = ajaxRequest
|
||||||
|
where
|
||||||
|
args :: [ST]
|
||||||
|
args = captures <> queryArgs <> body <> fmap (fst . snd) headerArgs
|
||||||
|
|
||||||
|
captures :: [ST]
|
||||||
|
captures = fmap (F.captureArg) . filter F.isCapture $ req ^. F.reqUrl . F.path
|
||||||
|
|
||||||
|
queryArgs :: [ST]
|
||||||
|
queryArgs = fmap ((<>) "query" . view F.argName) queryParams
|
||||||
|
|
||||||
|
headerArgs :: [(T.Text, (T.Text, F.HeaderArg))]
|
||||||
|
headerArgs = fmap ( decapitalise . F.headerArgName &&&
|
||||||
|
JS.toValidFunctionName . (<>) "header" . F.headerArgName &&&
|
||||||
|
id )
|
||||||
|
(req ^. F.reqHeaders)
|
||||||
|
|
||||||
|
fname :: ST
|
||||||
|
fname = F.camelCase (req ^. F.funcName)
|
||||||
|
<> if null captures then "" else "With"
|
||||||
|
<> T.intercalate "And" (fmap capitalise captures)
|
||||||
|
|
||||||
|
queryParams :: [F.QueryArg]
|
||||||
|
queryParams = req ^.. F.reqUrl . F.queryStr . traverse
|
||||||
|
|
||||||
|
body :: [ST]
|
||||||
|
body = ["body" | req ^. F.reqBody]
|
||||||
|
|
||||||
|
wrapHeaders :: [(T.Text, T.Text)] -> [(T.Text, (T.Text, F.HeaderArg))] -> ST
|
||||||
|
wrapHeaders ihs hs =
|
||||||
|
"[" <>
|
||||||
|
(T.intercalate ", " (concat $
|
||||||
|
[wrapImplicitHeader <$> ihs | not $ null ihs] ++
|
||||||
|
[wrapHeader <$> hs | not $ null hs])) <>
|
||||||
|
"]"
|
||||||
|
|
||||||
|
wrapHeader :: (T.Text, (T.Text, F.HeaderArg)) -> ST
|
||||||
|
wrapHeader (h, (_, o)) = "RequestHeader " <> cs (show h) <> " " <> psHeaderArg o
|
||||||
|
|
||||||
|
wrapImplicitHeader :: (T.Text, T.Text) -> ST
|
||||||
|
wrapImplicitHeader (k, v) = "RequestHeader " <> cs (show k) <> " " <> cs (show v)
|
||||||
|
|
||||||
|
implicitHeaderArgs :: [(T.Text, T.Text)]
|
||||||
|
implicitHeaderArgs =
|
||||||
|
[ ("content-type", "application/json")
|
||||||
|
, ("accept", "application/json")
|
||||||
|
]
|
||||||
|
|
||||||
|
ajaxRequest :: ST
|
||||||
|
ajaxRequest = T.unlines $
|
||||||
|
typeSig :
|
||||||
|
(fname <> argString <> " = affjax $ defaultRequest") :
|
||||||
|
(" { method = " <> req ^. F.reqMethod) :
|
||||||
|
(" , url = " <> urlString) :
|
||||||
|
(" , headers = " <> wrapHeaders implicitHeaderArgs headerArgs) :
|
||||||
|
[" , content = Just body" | req ^. F.reqBody] ++
|
||||||
|
" }" :
|
||||||
|
[]
|
||||||
|
where
|
||||||
|
typeSig :: ST
|
||||||
|
typeSig = T.concat
|
||||||
|
[ fname
|
||||||
|
, " :: forall eff. "
|
||||||
|
, T.intercalate " -> " typedArgs
|
||||||
|
, if null args then "" else " -> "
|
||||||
|
, "Affjax eff Foreign"
|
||||||
|
]
|
||||||
|
|
||||||
|
typedArgs :: [ST]
|
||||||
|
typedArgs = concat $
|
||||||
|
[ fmap (const "String") captures
|
||||||
|
, fmap (const "Maybe String") queryArgs ]
|
||||||
|
<> [ bodyArgType ]
|
||||||
|
<> [ fmap (const "String") headerArgs ]
|
||||||
|
|
||||||
|
argString :: ST
|
||||||
|
argString = case T.unwords args of
|
||||||
|
"" -> ""
|
||||||
|
s -> " " <> s
|
||||||
|
|
||||||
|
urlString :: ST
|
||||||
|
urlString = T.concat
|
||||||
|
[ "\""
|
||||||
|
, cs $ settings ^. baseURL
|
||||||
|
, "/"
|
||||||
|
, psPathSegments $ req ^.. F.reqUrl . F.path . traverse
|
||||||
|
, if null queryParams then "\"" else "?\" <> " <> psParams queryParams
|
||||||
|
]
|
||||||
|
|
||||||
|
bodyArgType :: [ST]
|
||||||
|
bodyArgType = [ "String" | req ^. F.reqBody ]
|
||||||
|
|
||||||
|
-- | Show HeaderArg instance from Servant.JS, changes to use PureScript
|
||||||
|
-- monoidal bindings
|
||||||
|
psHeaderArg :: F.HeaderArg -> ST
|
||||||
|
psHeaderArg (F.HeaderArg n) = JS.toValidFunctionName ("header" <> n)
|
||||||
|
psHeaderArg (F.ReplaceHeaderArg n p)
|
||||||
|
| pn `T.isPrefixOf` p = pv <> " <> \"" <> rp <> "\""
|
||||||
|
| pn `T.isSuffixOf` p = "\"" <> rp <> "\" <> " <> pv
|
||||||
|
| pn `T.isInfixOf` p = "\"" <> T.replace pn ("\" + " <> pv <> " + \"") p <> "\""
|
||||||
|
| otherwise = p
|
||||||
|
where
|
||||||
|
pv = JS.toValidFunctionName ("header" <> n)
|
||||||
|
pn = "{" <> n <> "}"
|
||||||
|
rp = T.replace pn "" p
|
||||||
|
|
||||||
|
-- | Default PureScript settings: specifies an empty base URL
|
||||||
|
defaultSettings :: PSSettings
|
||||||
|
defaultSettings = PSSettings "" "Util"
|
||||||
|
|
||||||
|
-- | Capitalise a string for use in PureScript variable name
|
||||||
|
capitalise :: (ConvertibleStrings s String, ConvertibleStrings String s) => s -> s
|
||||||
|
capitalise = cs . capitalise' . cs
|
||||||
|
|
||||||
|
capitalise' :: String -> String
|
||||||
|
capitalise' [] = []
|
||||||
|
capitalise' (x:xs) = [toUpper x] <> xs
|
||||||
|
|
||||||
|
-- | Decapitalise a string for use as a Purescript variable name
|
||||||
|
decapitalise :: (ConvertibleStrings s String, ConvertibleStrings String s) => s -> s
|
||||||
|
decapitalise = cs . decapitalise' . cs
|
||||||
|
|
||||||
|
decapitalise' :: String -> String
|
||||||
|
decapitalise' [] = []
|
||||||
|
decapitalise' (x:xs) = [toLower x] <> xs
|
||||||
|
|
||||||
|
-- | Turn a list of path segments into a URL string
|
||||||
|
psPathSegments :: [F.Segment] -> ST
|
||||||
|
psPathSegments = T.intercalate "/" . fmap psSegmentToStr
|
||||||
|
|
||||||
|
-- | Turn an individual path segment into a PureScript variable handler
|
||||||
|
psSegmentToStr :: F.Segment -> ST
|
||||||
|
psSegmentToStr (F.Segment (F.Static s)) = s
|
||||||
|
psSegmentToStr (F.Segment (F.Cap s)) = "\" <> encodeURIComponent " <> s <> " <> \""
|
||||||
|
|
||||||
|
-- | Turn a list of query string params into a URL string
|
||||||
|
psParams :: [F.QueryArg] -> ST
|
||||||
|
psParams qa = "(intercalate \"&\" <<< catMaybes $ [" <> T.intercalate ", " (psParamToStr <$> qa) <> "])"
|
||||||
|
|
||||||
|
-- | Turn an individual query string param into a PureScript variable handler
|
||||||
|
--
|
||||||
|
-- Must handle Maybe String as the input value
|
||||||
|
psParamToStr :: F.QueryArg -> ST
|
||||||
|
psParamToStr qarg = case qarg ^. F.argType of
|
||||||
|
F.Normal -> "((\"" <> name <> "=\" <>) <<< encodeURIComponent) <$> " <> qname
|
||||||
|
F.List -> "((\"" <> name <> "[]=\" <>) <<< encodeURIComponent) <$> " <> qname
|
||||||
|
F.Flag -> "\"" <> name <> "=\""
|
||||||
|
where
|
||||||
|
name = qarg ^. F.argName
|
||||||
|
qname = "query" <> name
|
65
servant-purescript/test/Spec.hs
Normal file
65
servant-purescript/test/Spec.hs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
{-# LANGUAGE DataKinds #-}
|
||||||
|
{-# LANGUAGE FlexibleInstances #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
{-# LANGUAGE TypeFamilies #-}
|
||||||
|
{-# LANGUAGE TypeOperators #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Control.Monad
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Data.Either (isRight)
|
||||||
|
import Data.Proxy
|
||||||
|
import Data.String.Conversions
|
||||||
|
import qualified Data.Text.IO as ST
|
||||||
|
import qualified Language.PureScript as P
|
||||||
|
import Servant.API
|
||||||
|
import Servant.Foreign as F
|
||||||
|
import Servant.JS
|
||||||
|
import Servant.PureScript
|
||||||
|
import Test.Hspec
|
||||||
|
import qualified Text.Parsec as TP
|
||||||
|
|
||||||
|
|
||||||
|
type TestAPI = "simple" :> ReqBody '[JSON] String :> Post '[JSON] Bool
|
||||||
|
:<|> "has.extension" :> Get '[JSON] Bool
|
||||||
|
|
||||||
|
type TopLevelRawAPI = "rwa" :> Get '[JSON] Int
|
||||||
|
:<|> Raw
|
||||||
|
|
||||||
|
type HeaderHandlingAPI = "oiy" :> Header "Foo" String :> Get '[JSON] Int
|
||||||
|
|
||||||
|
type QueryHandlingAPI = "urq" :> QueryParam "Bar" String :> Get '[JSON] Int
|
||||||
|
|
||||||
|
-- | 'Raw' has 'Foreign', but its type is @Method -> Req@, which doesn't have a 'GenerateList'
|
||||||
|
-- instance. Since @Foreign.Method@ is not exported, we need to keep it polymorphic.
|
||||||
|
instance {-# OVERLAPPABLE #-} GenerateList (a -> F.Req) where
|
||||||
|
generateList _ = []
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = hspec spec
|
||||||
|
|
||||||
|
spec :: Spec
|
||||||
|
spec = describe "generateJS" $ do
|
||||||
|
it "should generate valid purescript" $ do
|
||||||
|
let m = generatePSModule defaultSettings "Main" (Proxy :: Proxy TestAPI)
|
||||||
|
shouldParse m
|
||||||
|
it "should use non-empty function names" $ do
|
||||||
|
let m = generatePSModule defaultSettings "Main" (Proxy :: Proxy TopLevelRawAPI)
|
||||||
|
shouldParse m
|
||||||
|
(cs m :: String) `shouldContain` "getRwa :: "
|
||||||
|
it "should generate valid header variables" $ do
|
||||||
|
let m = generatePSModule defaultSettings "Main" (Proxy :: Proxy HeaderHandlingAPI)
|
||||||
|
shouldParse m
|
||||||
|
(cs m :: String) `shouldContain` "String"
|
||||||
|
(cs m :: String) `shouldContain` "headerFoo"
|
||||||
|
(cs m :: String) `shouldContain` "RequestHeader \"foo\" headerFoo"
|
||||||
|
it "should generate valid query params" $ do
|
||||||
|
let m = generatePSModule defaultSettings "Main" (Proxy :: Proxy QueryHandlingAPI)
|
||||||
|
shouldParse m
|
||||||
|
|
||||||
|
|
||||||
|
shouldParse :: ST -> Expectation
|
||||||
|
shouldParse =
|
||||||
|
(`shouldSatisfy` isRight) . (TP.runParser P.parseModule (P.ParseState 0) "" <=< P.lex "") . cs
|
Loading…
Reference in a new issue