2015-10-02 13:59:54 +02:00
|
|
|
{-#LANGUAGE OverloadedStrings #-}
|
2015-09-21 12:31:00 +02:00
|
|
|
module Servant.JS.Internal
|
|
|
|
( JavaScriptGenerator
|
2015-09-22 11:21:04 +02:00
|
|
|
, CommonGeneratorOptions(..)
|
|
|
|
, defCommonGeneratorOptions
|
2015-09-21 12:31:00 +02:00
|
|
|
, AjaxReq
|
|
|
|
, jsSegments
|
|
|
|
, segmentToStr
|
|
|
|
, segmentTypeToStr
|
|
|
|
, jsParams
|
|
|
|
, jsGParams
|
|
|
|
, jsMParams
|
|
|
|
, paramToStr
|
2015-09-22 12:17:43 +02:00
|
|
|
, toValidFunctionName
|
|
|
|
, toJSHeader
|
2015-09-21 12:31:00 +02:00
|
|
|
-- re-exports
|
|
|
|
, (:<|>)(..)
|
|
|
|
, (:>)
|
|
|
|
, defReq
|
|
|
|
, reqHeaders
|
|
|
|
, HasForeign(..)
|
|
|
|
, HeaderArg(..)
|
|
|
|
, concatCase
|
|
|
|
, snakeCase
|
|
|
|
, camelCase
|
|
|
|
, ReqBody
|
|
|
|
, JSON
|
|
|
|
, FormUrlEncoded
|
|
|
|
, Post
|
|
|
|
, Get
|
|
|
|
, Raw
|
|
|
|
, Header
|
2015-09-23 18:26:05 +02:00
|
|
|
) where
|
2014-11-25 01:36:34 +01:00
|
|
|
|
2015-09-23 20:30:45 +02:00
|
|
|
import Control.Lens ((^.))
|
2015-09-22 12:17:43 +02:00
|
|
|
import qualified Data.CharSet as Set
|
|
|
|
import qualified Data.CharSet.Unicode.Category as Set
|
|
|
|
import Data.Monoid
|
|
|
|
import qualified Data.Text as T
|
2015-10-02 13:59:54 +02:00
|
|
|
import Data.Text (Text)
|
2015-09-22 12:17:43 +02:00
|
|
|
import Servant.Foreign
|
2014-11-25 01:36:34 +01:00
|
|
|
|
2015-09-21 12:31:00 +02:00
|
|
|
type AjaxReq = Req
|
2014-11-25 01:36:34 +01:00
|
|
|
|
2015-07-22 19:23:31 +02:00
|
|
|
-- A 'JavascriptGenerator' just takes the data found in the API type
|
2015-10-02 13:59:54 +02:00
|
|
|
-- for each endpoint and generates Javascript code in a Text. Several
|
2015-07-22 19:23:31 +02:00
|
|
|
-- generators are available in this package.
|
2015-10-02 13:59:54 +02:00
|
|
|
type JavaScriptGenerator = [Req] -> Text
|
2015-01-03 18:52:18 +01:00
|
|
|
|
2015-09-22 11:21:04 +02:00
|
|
|
-- | This structure is used by specific implementations to let you
|
|
|
|
-- customize the output
|
|
|
|
data CommonGeneratorOptions = CommonGeneratorOptions
|
|
|
|
{
|
2015-10-02 13:59:54 +02:00
|
|
|
functionNameBuilder :: FunctionName -> Text -- ^ function generating function names
|
|
|
|
, requestBody :: Text -- ^ name used when a user want to send the request body (to let you redefine it)
|
|
|
|
, successCallback :: Text -- ^ name of the callback parameter when the request was successful
|
|
|
|
, errorCallback :: Text -- ^ name of the callback parameter when the request reported an error
|
|
|
|
, moduleName :: Text -- ^ namespace on which we define the foreign function (empty mean local var)
|
|
|
|
, urlPrefix :: Text -- ^ a prefix we should add to the Url in the codegen
|
2015-09-22 11:21:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
-- | Default options.
|
|
|
|
--
|
|
|
|
-- @
|
|
|
|
-- > defCommonGeneratorOptions = CommonGeneratorOptions
|
|
|
|
-- > { functionNameBuilder = camelCase
|
|
|
|
-- > , requestBody = "body"
|
|
|
|
-- > , successCallback = "onSuccess"
|
|
|
|
-- > , errorCallback = "onError"
|
|
|
|
-- > , moduleName = ""
|
|
|
|
-- > , urlPrefix = ""
|
|
|
|
-- > }
|
|
|
|
-- @
|
|
|
|
defCommonGeneratorOptions :: CommonGeneratorOptions
|
|
|
|
defCommonGeneratorOptions = CommonGeneratorOptions
|
|
|
|
{
|
|
|
|
functionNameBuilder = camelCase
|
|
|
|
, requestBody = "body"
|
|
|
|
, successCallback = "onSuccess"
|
|
|
|
, errorCallback = "onError"
|
|
|
|
, moduleName = ""
|
|
|
|
, urlPrefix = ""
|
|
|
|
}
|
|
|
|
|
2015-09-22 12:17:43 +02:00
|
|
|
-- | Attempts to reduce the function name provided to that allowed by @'Foreign'@.
|
|
|
|
--
|
|
|
|
-- 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?
|
2015-10-02 13:59:54 +02:00
|
|
|
toValidFunctionName :: Text -> Text
|
|
|
|
toValidFunctionName t =
|
|
|
|
case T.uncons t of
|
|
|
|
Just (x,xs) ->
|
|
|
|
setFirstChar x `T.cons` T.filter remainder xs
|
|
|
|
Nothing -> "_"
|
|
|
|
where
|
|
|
|
setFirstChar c = if firstChar c then c else '_'
|
2015-10-04 23:32:31 +02:00
|
|
|
firstChar c = prefixOK c || Set.member c firstLetterOK
|
|
|
|
remainder c = prefixOK c || Set.member c remainderOK
|
2015-09-22 12:17:43 +02:00
|
|
|
prefixOK c = c `elem` ['$','_']
|
2015-10-04 23:32:31 +02:00
|
|
|
firstLetterOK = mconcat
|
|
|
|
[ Set.lowercaseLetter
|
|
|
|
, Set.uppercaseLetter
|
|
|
|
, Set.titlecaseLetter
|
|
|
|
, Set.modifierLetter
|
|
|
|
, Set.otherLetter
|
|
|
|
, Set.letterNumber
|
|
|
|
]
|
2015-09-22 12:17:43 +02:00
|
|
|
remainderOK = firstLetterOK
|
2015-10-04 23:32:31 +02:00
|
|
|
<> mconcat
|
|
|
|
[ Set.nonSpacingMark
|
|
|
|
, Set.spacingCombiningMark
|
|
|
|
, Set.decimalNumber
|
|
|
|
, Set.connectorPunctuation
|
|
|
|
]
|
2015-09-22 12:17:43 +02:00
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
toJSHeader :: HeaderArg -> Text
|
2015-09-22 12:17:43 +02:00
|
|
|
toJSHeader (HeaderArg n) = toValidFunctionName ("header" <> n)
|
|
|
|
toJSHeader (ReplaceHeaderArg n p)
|
2015-10-02 13:59:54 +02:00
|
|
|
| pn `T.isPrefixOf` p = pv <> " + \"" <> rp <> "\""
|
|
|
|
| pn `T.isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
|
|
|
|
| pn `T.isInfixOf` p = "\"" <> (T.replace pn ("\" + " <> pv <> " + \"") p)
|
2015-09-22 12:17:43 +02:00
|
|
|
<> "\""
|
|
|
|
| otherwise = p
|
|
|
|
where
|
|
|
|
pv = toValidFunctionName ("header" <> n)
|
|
|
|
pn = "{" <> n <> "}"
|
2015-10-02 13:59:54 +02:00
|
|
|
rp = T.replace pn "" p
|
2015-09-22 12:17:43 +02:00
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
jsSegments :: [Segment] -> Text
|
2015-01-03 18:52:18 +01:00
|
|
|
jsSegments [] = ""
|
2015-10-02 13:59:54 +02:00
|
|
|
jsSegments [x] = "/" <> segmentToStr x False
|
|
|
|
jsSegments (x:xs) = "/" <> segmentToStr x True <> jsSegments xs
|
2015-01-03 18:52:18 +01:00
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
segmentToStr :: Segment -> Bool -> Text
|
2015-01-03 18:52:18 +01:00
|
|
|
segmentToStr (Segment st ms) notTheEnd =
|
2015-10-02 13:59:54 +02:00
|
|
|
segmentTypeToStr st <> jsMParams ms <> if notTheEnd then "" else "'"
|
2015-01-03 18:52:18 +01:00
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
segmentTypeToStr :: SegmentType -> Text
|
2015-01-03 18:52:18 +01:00
|
|
|
segmentTypeToStr (Static s) = s
|
2015-10-02 13:59:54 +02:00
|
|
|
segmentTypeToStr (Cap s) = "' + encodeURIComponent(" <> s <> ") + '"
|
2015-01-03 18:52:18 +01:00
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
jsGParams :: Text -> [QueryArg] -> Text
|
2015-09-21 12:31:00 +02:00
|
|
|
jsGParams _ [] = ""
|
|
|
|
jsGParams _ [x] = paramToStr x False
|
2015-10-02 13:59:54 +02:00
|
|
|
jsGParams s (x:xs) = paramToStr x True <> s <> jsGParams s xs
|
2015-01-03 18:52:18 +01:00
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
jsParams :: [QueryArg] -> Text
|
2015-01-03 18:52:18 +01:00
|
|
|
jsParams = jsGParams "&"
|
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
jsMParams :: [MatrixArg] -> Text
|
2015-01-03 18:52:18 +01:00
|
|
|
jsMParams [] = ""
|
2015-10-02 13:59:54 +02:00
|
|
|
jsMParams xs = ";" <> jsGParams ";" xs
|
2014-11-25 01:36:34 +01:00
|
|
|
|
2015-10-02 13:59:54 +02:00
|
|
|
paramToStr :: QueryArg -> Bool -> Text
|
2014-11-25 01:36:34 +01:00
|
|
|
paramToStr qarg notTheEnd =
|
|
|
|
case qarg ^. argType of
|
|
|
|
Normal -> name
|
2015-10-02 13:59:54 +02:00
|
|
|
<> "=' + encodeURIComponent("
|
|
|
|
<> name
|
|
|
|
<> if notTheEnd then ") + '" else ")"
|
|
|
|
Flag -> name <> "="
|
2014-11-25 01:36:34 +01:00
|
|
|
List -> name
|
2015-10-02 13:59:54 +02:00
|
|
|
<> "[]=' + encodeURIComponent("
|
|
|
|
<> name
|
|
|
|
<> if notTheEnd then ") + '" else ")"
|
2014-11-25 01:36:34 +01:00
|
|
|
where name = qarg ^. argName
|