git.iko.soy/typeable/servant
on master help sign in
Merge pull request #243 from arianvp/foreign-text

Change servant-js and servant-foreign to use text

commit05623f54d4001549aa9859e5b89bb8492c5fff66 authorJulian Arni <jkarni@gmail.com> dateThu 8 Oct 2015, 13:59 (11 years ago) parent9245c9f568254d
13 files changed+224−194
servant-client/CHANGELOG.md+1 servant-examples/tutorial/T9.hs+5−4 servant-foreign/CHANGELOG.md+2−2 servant-foreign/servant-foreign.cabal+2−1 servant-foreign/src/Servant/Foreign.hs+26−29 servant-js/src/Servant/JS.hs+6−3 servant-js/src/Servant/JS/Angular.hs+24−22 servant-js/src/Servant/JS/Axios.hs+19−18 servant-js/src/Servant/JS/Internal.hs+57−55 servant-js/src/Servant/JS/JQuery.hs+18−16 servant-js/src/Servant/JS/Vanilla.hs+18−16 servant-js/test/Servant/JSSpec.hs+43−27 servant-js/test/Servant/JSSpec/CustomHeaders.hs+3−1
servant-client/CHANGELOG.md+1
@@ -1,6 +1,7 @@
11HEAD
22----
33
4+* Use the `text` package instead of `String`.
45* Support for the `HttpVersion`, `IsSecure`, `RemoteHost` and `Vault` combinators
56* Added support for `path` on `BaseUrl`.
67* `client` now takes an explicit `Manager` argument.
servant-examples/tutorial/T9.hs+5−4
@@ -16,6 +16,7 @@ import Servant.JS
1616import System.Random
1717
1818import qualified Data.Text as T
19+import qualified Data.Text.IO as TIO
1920import qualified Language.Javascript.JQuery as JQ
2021
2122data Point = Point
@@ -91,14 +92,14 @@ server' :: Server API'
9192server' = server
9293 :<|> serveDirectory "tutorial/t9"
9394
94−apiJS :: String
95+apiJS :: Text
9596apiJS = jsForAPI api jquery
9697
9798writeJSFiles :: IO ()
9899writeJSFiles = do
99− writeFile "tutorial/t9/api.js" apiJS
100− jq <- readFile =<< JQ.file
101− writeFile "tutorial/t9/jq.js" jq
100+ TIO.writeFile "tutorial/t9/api.js" apiJS
101+ jq <- TIO.readFile =<< JQ.file
102+ TIO.writeFile "tutorial/t9/jq.js" jq
102103
103104app :: Application
104105app = serve api' server'
servant-foreign/CHANGELOG.md+2−2
@@ -1,4 +1,4 @@
1−0.5
1+HEAD
22-----
3−
3+* Use the `text` package instead of `String`.
44* Extract javascript-oblivious types and helpers to *servant-foreign*
servant-foreign/servant-foreign.cabal+2−1
@@ -29,6 +29,7 @@ library
2929 build-depends: base == 4.*
3030 , lens == 4.*
3131 , servant == 0.5.*
32+ , text >= 1.2 && < 1.3
3233 hs-source-dirs: src
3334 default-language: Haskell2010
34− ghc-options: -Wall
35+ ghc-options: -Wall
servant-foreign/src/Servant/Foreign.hs+26−29
@@ -12,6 +12,7 @@
1212{-# LANGUAGE TypeFamilies #-}
1313{-# LANGUAGE TypeOperators #-}
1414{-# LANGUAGE UndecidableInstances #-}
15+{-# LANGUAGE OverloadedStrings #-}
1516
1617-- | Generalizes all the data needed to make code generation work with
1718-- arbitrary programming languages.
@@ -46,43 +47,39 @@ module Servant.Foreign
4647 , module Servant.API
4748 ) where
4849
49−#if !MIN_VERSION_base(4,8,0)
50−import Control.Applicative
51−#endif
5250import Control.Lens (makeLenses, (%~), (&), (.~),
5351 (<>~), _last)
54−import Data.Char (toLower, toUpper)
55−import Data.List
52+import Data.Monoid ((<>))
53+import Data.Text
5654import Data.Proxy
5755import GHC.Exts (Constraint)
5856import GHC.TypeLits
5957import Servant.API
58+import Prelude hiding (concat)
6059
6160-- | Function name builder that simply concat each part together
62−concatCase :: FunctionName -> String
61+concatCase :: FunctionName -> Text
6362concatCase = concat
6463
6564-- | Function name builder using the snake_case convention.
6665-- each part is separated by a single underscore character.
67−snakeCase :: FunctionName -> String
66+snakeCase :: FunctionName -> Text
6867snakeCase = intercalate "_"
6968
7069-- | Function name builder using the CamelCase convention.
7170-- each part begins with an upper case character.
72−camelCase :: FunctionName -> String
71+camelCase :: FunctionName -> Text
7372camelCase [] = ""
7473camelCase (p:ps) = concat $ p : camelCase' ps
7574 where camelCase' [] = []
76− camelCase' (r:rs) = capitalize r : camelCase' rs
77− capitalize [] = []
78− capitalize (x:xs) = toUpper x : xs
75+ camelCase' (r:rs) = toUpper r : camelCase' rs
7976
80−type Arg = String
77+type Arg = Text
8178
8279data Segment = Segment { _segment :: SegmentType, _matrix :: [MatrixArg] }
8380 deriving (Eq, Show)
8481
85−data SegmentType = Static String -- ^ a static path segment. like "/foo"
82+data SegmentType = Static Text -- ^ a static path segment. like "/foo"
8683 | Cap Arg -- ^ a capture. like "/:userid"
8784 deriving (Eq, Show)
8885
@@ -100,11 +97,11 @@ data QueryArg = QueryArg
10097 } deriving (Eq, Show)
10198
10299data HeaderArg = HeaderArg
103− { headerArgName :: String
100+ { headerArgName :: Text
104101 }
105102 | ReplaceHeaderArg
106− { headerArgName :: String
107− , headerPattern :: String
103+ { headerArgName :: Text
104+ , headerPattern :: Text
108105 } deriving (Eq, Show)
109106
110107
@@ -118,8 +115,8 @@ data Url = Url
118115defUrl :: Url
119116defUrl = Url [] []
120117
121−type FunctionName = [String]
122−type Method = String
118+type FunctionName = [Text]
119+type Method = Text
123120
124121data Req = Req
125122 { _reqUrl :: Url
@@ -175,7 +172,7 @@ instance (KnownSymbol sym, HasForeign sublayout)
175172 req & reqUrl.path <>~ [Segment (Cap str) []]
176173 & funcName %~ (++ ["by", str])
177174
178− where str = symbolVal (Proxy :: Proxy sym)
175+ where str = pack . symbolVal $ (Proxy :: Proxy sym)
179176
180177instance Elem JSON list => HasForeign (Delete list a) where
181178 type Foreign (Delete list a) = Req
@@ -198,7 +195,7 @@ instance (KnownSymbol sym, HasForeign sublayout)
198195 foreignFor Proxy req =
199196 foreignFor subP (req & reqHeaders <>~ [HeaderArg hname])
200197
201− where hname = symbolVal (Proxy :: Proxy sym)
198+ where hname = pack . symbolVal $ (Proxy :: Proxy sym)
202199 subP = Proxy :: Proxy sublayout
203200
204201instance Elem JSON list => HasForeign (Post list a) where
@@ -223,7 +220,7 @@ instance (KnownSymbol sym, HasForeign sublayout)
223220 foreignFor (Proxy :: Proxy sublayout) $
224221 req & reqUrl.queryStr <>~ [QueryArg str Normal]
225222
226− where str = symbolVal (Proxy :: Proxy sym)
223+ where str = pack . symbolVal $ (Proxy :: Proxy sym)
227224
228225instance (KnownSymbol sym, HasForeign sublayout)
229226 => HasForeign (QueryParams sym a :> sublayout) where
@@ -233,7 +230,7 @@ instance (KnownSymbol sym, HasForeign sublayout)
233230 foreignFor (Proxy :: Proxy sublayout) $
234231 req & reqUrl.queryStr <>~ [QueryArg str List]
235232
236− where str = symbolVal (Proxy :: Proxy sym)
233+ where str = pack . symbolVal $ (Proxy :: Proxy sym)
237234
238235instance (KnownSymbol sym, HasForeign sublayout)
239236 => HasForeign (QueryFlag sym :> sublayout) where
@@ -243,7 +240,7 @@ instance (KnownSymbol sym, HasForeign sublayout)
243240 foreignFor (Proxy :: Proxy sublayout) $
244241 req & reqUrl.queryStr <>~ [QueryArg str Flag]
245242
246− where str = symbolVal (Proxy :: Proxy sym)
243+ where str = pack . symbolVal $ (Proxy :: Proxy sym)
247244
248245instance (KnownSymbol sym, HasForeign sublayout)
249246 => HasForeign (MatrixParam sym a :> sublayout) where
@@ -253,8 +250,8 @@ instance (KnownSymbol sym, HasForeign sublayout)
253250 foreignFor (Proxy :: Proxy sublayout) $
254251 req & reqUrl.path._last.matrix <>~ [QueryArg strArg Normal]
255252
256− where str = symbolVal (Proxy :: Proxy sym)
257− strArg = str ++ "Value"
253+ where str = pack . symbolVal $ (Proxy :: Proxy sym)
254+ strArg = str <> "Value"
258255
259256instance (KnownSymbol sym, HasForeign sublayout)
260257 => HasForeign (MatrixParams sym a :> sublayout) where
@@ -264,7 +261,7 @@ instance (KnownSymbol sym, HasForeign sublayout)
264261 foreignFor (Proxy :: Proxy sublayout) $
265262 req & reqUrl.path._last.matrix <>~ [QueryArg str List]
266263
267− where str = symbolVal (Proxy :: Proxy sym)
264+ where str = pack . symbolVal $ (Proxy :: Proxy sym)
268265
269266instance (KnownSymbol sym, HasForeign sublayout)
270267 => HasForeign (MatrixFlag sym :> sublayout) where
@@ -274,13 +271,13 @@ instance (KnownSymbol sym, HasForeign sublayout)
274271 foreignFor (Proxy :: Proxy sublayout) $
275272 req & reqUrl.path._last.matrix <>~ [QueryArg str Flag]
276273
277− where str = symbolVal (Proxy :: Proxy sym)
274+ where str = pack . symbolVal $ (Proxy :: Proxy sym)
278275
279276instance HasForeign Raw where
280277 type Foreign Raw = Method -> Req
281278
282279 foreignFor Proxy req method =
283− req & funcName %~ ((toLower <$> method) :)
280+ req & funcName %~ ((toLower method) :)
284281 & reqMethod .~ method
285282
286283instance (Elem JSON list, HasForeign sublayout) => HasForeign (ReqBody list a :> sublayout) where
@@ -299,7 +296,7 @@ instance (KnownSymbol path, HasForeign sublayout)
299296 req & reqUrl.path <>~ [Segment (Static str) []]
300297 & funcName %~ (++ [str])
301298
302− where str = map (\c -> if c == '.' then '_' else c) $ symbolVal (Proxy :: Proxy path)
299+ where str = Data.Text.map (\c -> if c == '.' then '_' else c) . pack . symbolVal $ (Proxy :: Proxy path)
303300
304301instance HasForeign sublayout => HasForeign (RemoteHost :> sublayout) where
305302 type Foreign (RemoteHost :> sublayout) = Foreign sublayout
servant-js/src/Servant/JS.hs+6−3
@@ -39,7 +39,7 @@
3939-- Let's keep it simple and produce vanilla Javascript code with the default options.
4040--
4141-- @
42−-- jsCode :: String
42+-- jsCode :: Text
4343-- jsCode = 'jsForAPI' api 'vanillaJS'
4444-- @
4545--
@@ -60,7 +60,7 @@
6060-- All you need to do to use it is to use 'vanillaJSWith' and pass it @myOptions@.
6161--
6262-- @
63−-- jsCodeWithMyOptions :: String
63+-- jsCodeWithMyOptions :: Text
6464-- jsCodeWithMyOptions = 'jsForAPI' api ('vanillaJSWith' myOptions)
6565-- @
6666--
@@ -112,7 +112,10 @@ module Servant.JS
112112 , GenerateList(..)
113113 ) where
114114
115+import Prelude hiding (writeFile)
115116import Data.Proxy
117+import Data.Text
118+import Data.Text.IO (writeFile)
116119import Servant.JS.Angular
117120import Servant.JS.Axios
118121import Servant.JS.Internal
@@ -131,7 +134,7 @@ javascript p = foreignFor p defReq
131134jsForAPI :: (HasForeign api, GenerateList (Foreign api))
132135 => Proxy api -- ^ proxy for your API type
133136 -> JavaScriptGenerator -- ^ js code generator to use (angular, vanilla js, jquery, others)
134− -> String -- ^ a string that you can embed in your pages or write to a file
137+ -> Text -- ^ a text that you can embed in your pages or write to a file
135138jsForAPI p gen = gen (listFromAPI p)
136139
137140-- | Directly generate all the javascript functions for your API
servant-js/src/Servant/JS/Angular.hs+24−22
@@ -1,17 +1,19 @@
1+{-#LANGUAGE OverloadedStrings #-}
12module Servant.JS.Angular where
23
34import Control.Lens
4−import Data.List
55import Data.Monoid
6+import qualified Data.Text as T
7+import Data.Text (Text)
68import Servant.Foreign
79import Servant.JS.Internal
810
911-- | Options specific to the angular code generator
1012data AngularOptions = AngularOptions
11− { serviceName :: String -- ^ When generating code with wrapInService,
13+ { serviceName :: Text -- ^ When generating code with wrapInService,
1214 -- name of the service to generate
13− , prologue :: String -> String -> String -- ^ beginning of the service definition
14− , epilogue :: String -- ^ end of the service definition
15+ , prologue :: Text -> Text -> Text -- ^ beginning of the service definition
16+ , epilogue :: Text -- ^ end of the service definition
1517 }
1618
1719-- | Default options for the Angular codegen. Used by 'wrapInService'.
@@ -34,12 +36,12 @@ angularService ngOpts = angularServiceWith ngOpts defCommonGeneratorOptions
3436angularServiceWith :: AngularOptions -> CommonGeneratorOptions -> JavaScriptGenerator
3537angularServiceWith ngOpts opts reqs =
3638 prologue ngOpts svc mName
37− <> intercalate "," (map generator reqs) <>
39+ <> T.intercalate "," (map generator reqs) <>
3840 epilogue ngOpts
3941 where
4042 generator req = generateAngularJSWith ngOpts opts req
4143 svc = serviceName ngOpts
42− mName = if null (moduleName opts)
44+ mName = if moduleName opts == ""
4345 then "app."
4446 else moduleName opts <> "."
4547
@@ -50,14 +52,14 @@ angular ngopts = angularWith ngopts defCommonGeneratorOptions
5052
5153-- | Generate regular javascript functions that use the $http service.
5254angularWith :: AngularOptions -> CommonGeneratorOptions -> JavaScriptGenerator
53−angularWith ngopts opts = intercalate "\n\n" . map (generateAngularJSWith ngopts opts)
55+angularWith ngopts opts = T.intercalate "\n\n" . map (generateAngularJSWith ngopts opts)
5456
5557-- | js codegen using $http service from Angular using default options
56−generateAngularJS :: AngularOptions -> AjaxReq -> String
58+generateAngularJS :: AngularOptions -> AjaxReq -> Text
5759generateAngularJS ngOpts = generateAngularJSWith ngOpts defCommonGeneratorOptions
5860
5961-- | js codegen using $http service from Angular
60−generateAngularJSWith :: AngularOptions -> CommonGeneratorOptions -> AjaxReq -> String
62+generateAngularJSWith :: AngularOptions -> CommonGeneratorOptions -> AjaxReq -> Text
6163generateAngularJSWith ngOptions opts req = "\n" <>
6264 fname <> fsep <> " function(" <> argsStr <> ")\n"
6365 <> "{\n"
@@ -69,7 +71,7 @@ generateAngularJSWith ngOptions opts req = "\n" <>
6971 <> " });\n"
7072 <> "}\n"
7173
72− where argsStr = intercalate ", " args
74+ where argsStr = T.intercalate ", " args
7375 args = http
7476 ++ captures
7577 ++ map (view argName) queryparams
@@ -79,7 +81,7 @@ generateAngularJSWith ngOptions opts req = "\n" <>
7981 -- If we want to generate Top Level Function, they must depend on
8082 -- the $http service, if we generate a service, the functions will
8183 -- inherit this dependency from the service
82− http = case length (serviceName ngOptions) of
84+ http = case T.length (serviceName ngOptions) of
8385 0 -> ["$http"]
8486 _ -> []
8587
@@ -104,12 +106,12 @@ generateAngularJSWith ngOptions opts req = "\n" <>
104106 reqheaders =
105107 if null hs
106108 then ""
107− else " , headers: { " ++ headersStr ++ " }\n"
109+ else " , headers: { " <> headersStr <> " }\n"
108110
109− where headersStr = intercalate ", " $ map headerStr hs
110− headerStr header = "\"" ++
111− headerArgName header ++
112− "\": " ++ toJSHeader header
111+ where headersStr = T.intercalate ", " $ map headerStr hs
112+ headerStr header = "\"" <>
113+ headerArgName header <>
114+ "\": " <> toJSHeader header
113115
114116 namespace =
115117 if hasService
@@ -118,9 +120,9 @@ generateAngularJSWith ngOptions opts req = "\n" <>
118120 then "var "
119121 else (moduleName opts) <> "."
120122 where
121− hasNoModule = null (moduleName opts)
123+ hasNoModule = moduleName opts == ""
122124
123− hasService = not $ null (serviceName ngOptions)
125+ hasService = serviceName ngOptions /= ""
124126
125127 fsep = if hasService then ":" else " ="
126128
@@ -129,13 +131,13 @@ generateAngularJSWith ngOptions opts req = "\n" <>
129131 method = req ^. reqMethod
130132 url = if url' == "'" then "'/'" else url'
131133 url' = "'"
132− ++ urlPrefix opts
133− ++ urlArgs
134− ++ queryArgs
134+ <> urlPrefix opts
135+ <> urlArgs
136+ <> queryArgs
135137
136138 urlArgs = jsSegments
137139 $ req ^.. reqUrl.path.traverse
138140
139141 queryArgs = if null queryparams
140142 then ""
141− else " + '?" ++ jsParams queryparams
143+ else " + '?" <> jsParams queryparams
servant-js/src/Servant/JS/Axios.hs+19−18
@@ -1,9 +1,10 @@
1+{-#LANGUAGE OverloadedStrings #-}
12module Servant.JS.Axios where
23
34import Control.Lens
4−import Data.Char (toLower)
5−import Data.List
65import Data.Monoid
6+import Data.Text (Text)
7+import qualified Data.Text as T
78import Servant.Foreign
89import Servant.JS.Internal
910
@@ -14,9 +15,9 @@ data AxiosOptions = AxiosOptions
1415 -- should be made using credentials
1516 withCredentials :: !Bool
1617 -- | the name of the cookie to use as a value for xsrf token
17− , xsrfCookieName :: !(Maybe String)
18+ , xsrfCookieName :: !(Maybe Text)
1819 -- | the name of the header to use as a value for xsrf token
19− , xsrfHeaderName :: !(Maybe String)
20+ , xsrfHeaderName :: !(Maybe Text)
2021 }
2122
2223-- | Default instance of the AxiosOptions
@@ -36,14 +37,14 @@ axios aopts = axiosWith aopts defCommonGeneratorOptions
3637
3738-- | Generate regular javascript functions that use the axios library.
3839axiosWith :: AxiosOptions -> CommonGeneratorOptions -> JavaScriptGenerator
39−axiosWith aopts opts = intercalate "\n\n" . map (generateAxiosJSWith aopts opts)
40+axiosWith aopts opts = T.intercalate "\n\n" . map (generateAxiosJSWith aopts opts)
4041
4142-- | js codegen using axios library using default options
42−generateAxiosJS :: AxiosOptions -> AjaxReq -> String
43+generateAxiosJS :: AxiosOptions -> AjaxReq -> Text
4344generateAxiosJS aopts = generateAxiosJSWith aopts defCommonGeneratorOptions
4445
4546-- | js codegen using axios library
46−generateAxiosJSWith :: AxiosOptions -> CommonGeneratorOptions -> AjaxReq -> String
47+generateAxiosJSWith :: AxiosOptions -> CommonGeneratorOptions -> AjaxReq -> Text
4748generateAxiosJSWith aopts opts req = "\n" <>
4849 fname <> " = function(" <> argsStr <> ")\n"
4950 <> "{\n"
@@ -57,7 +58,7 @@ generateAxiosJSWith aopts opts req = "\n" <>
5758 <> " });\n"
5859 <> "}\n"
5960
60− where argsStr = intercalate ", " args
61+ where argsStr = T.intercalate ", " args
6162 args = captures
6263 ++ map (view argName) queryparams
6364 ++ body
@@ -101,30 +102,30 @@ generateAxiosJSWith aopts opts req = "\n" <>
101102 then ""
102103 else " , headers: { " <> headersStr <> " }\n"
103104
104− where headersStr = intercalate ", " $ map headerStr hs
105− headerStr header = "\"" ++
106− headerArgName header ++
107− "\": " ++ toJSHeader header
105+ where headersStr = T.intercalate ", " $ map headerStr hs
106+ headerStr header = "\"" <>
107+ headerArgName header <>
108+ "\": " <> toJSHeader header
108109
109110 namespace =
110111 if hasNoModule
111112 then "var "
112113 else (moduleName opts) <> "."
113114 where
114− hasNoModule = null (moduleName opts)
115+ hasNoModule = moduleName opts == ""
115116
116117 fname = namespace <> (functionNameBuilder opts $ req ^. funcName)
117118
118− method = map toLower $ req ^. reqMethod
119+ method = T.toLower $ req ^. reqMethod
119120 url = if url' == "'" then "'/'" else url'
120121 url' = "'"
121− ++ urlPrefix opts
122− ++ urlArgs
123− ++ queryArgs
122+ <> urlPrefix opts
123+ <> urlArgs
124+ <> queryArgs
124125
125126 urlArgs = jsSegments
126127 $ req ^.. reqUrl.path.traverse
127128
128129 queryArgs = if null queryparams
129130 then ""
130− else " + '?" ++ jsParams queryparams
131+ else " + '?" <> jsParams queryparams
servant-js/src/Servant/JS/Internal.hs+57−55
@@ -1,3 +1,4 @@
1+{-#LANGUAGE OverloadedStrings #-}
12module Servant.JS.Internal
23 ( JavaScriptGenerator
34 , CommonGeneratorOptions(..)
@@ -34,28 +35,28 @@ module Servant.JS.Internal
3435import Control.Lens ((^.))
3536import qualified Data.CharSet as Set
3637import qualified Data.CharSet.Unicode.Category as Set
37−import Data.List
3838import Data.Monoid
3939import qualified Data.Text as T
40+import Data.Text (Text)
4041import Servant.Foreign
4142
4243type AjaxReq = Req
4344
4445-- A 'JavascriptGenerator' just takes the data found in the API type
45−-- for each endpoint and generates Javascript code in a String. Several
46+-- for each endpoint and generates Javascript code in a Text. Several
4647-- generators are available in this package.
47−type JavaScriptGenerator = [Req] -> String
48+type JavaScriptGenerator = [Req] -> Text
4849
4950-- | This structure is used by specific implementations to let you
5051-- customize the output
5152data CommonGeneratorOptions = CommonGeneratorOptions
5253 {
53− functionNameBuilder :: FunctionName -> String -- ^ function generating function names
54− , requestBody :: String -- ^ name used when a user want to send the request body (to let you redefine it)
55− , successCallback :: String -- ^ name of the callback parameter when the request was successful
56− , errorCallback :: String -- ^ name of the callback parameter when the request reported an error
57− , moduleName :: String -- ^ namespace on which we define the foreign function (empty mean local var)
58− , urlPrefix :: String -- ^ a prefix we should add to the Url in the codegen
54+ functionNameBuilder :: FunctionName -> Text -- ^ function generating function names
55+ , requestBody :: Text -- ^ name used when a user want to send the request body (to let you redefine it)
56+ , successCallback :: Text -- ^ name of the callback parameter when the request was successful
57+ , errorCallback :: Text -- ^ name of the callback parameter when the request reported an error
58+ , moduleName :: Text -- ^ namespace on which we define the foreign function (empty mean local var)
59+ , urlPrefix :: Text -- ^ a prefix we should add to the Url in the codegen
5960 }
6061
6162-- | Default options.
@@ -87,80 +88,81 @@ defCommonGeneratorOptions = CommonGeneratorOptions
8788-- Couldn't work out how to handle zero-width characters.
8889--
8990-- @TODO: specify better default function name, or throw error?
90−toValidFunctionName :: String -> String
91−toValidFunctionName (x:xs) = [setFirstChar x] <> filter remainder xs
91+toValidFunctionName :: Text -> Text
92+toValidFunctionName t =
93+ case T.uncons t of
94+ Just (x,xs) ->
95+ setFirstChar x `T.cons` T.filter remainder xs
96+ Nothing -> "_"
9297 where
9398 setFirstChar c = if firstChar c then c else '_'
94− firstChar c = prefixOK c || any (Set.member c) firstLetterOK
95− remainder c = prefixOK c || any (Set.member c) remainderOK
96− -- Valid prefixes
99+ firstChar c = prefixOK c || Set.member c firstLetterOK
100+ remainder c = prefixOK c || Set.member c remainderOK
97101 prefixOK c = c `elem` ['$','_']
98− -- Unicode character sets
99− firstLetterOK = [ Set.lowercaseLetter
100− , Set.uppercaseLetter
101− , Set.titlecaseLetter
102− , Set.modifierLetter
103− , Set.otherLetter
104− , Set.letterNumber ]
102+ firstLetterOK = mconcat
103+ [ Set.lowercaseLetter
104+ , Set.uppercaseLetter
105+ , Set.titlecaseLetter
106+ , Set.modifierLetter
107+ , Set.otherLetter
108+ , Set.letterNumber
109+ ]
105110 remainderOK = firstLetterOK
106− <> [ Set.nonSpacingMark
107− , Set.spacingCombiningMark
108− , Set.decimalNumber
109− , Set.connectorPunctuation ]
110−toValidFunctionName [] = "_"
111+ <> mconcat
112+ [ Set.nonSpacingMark
113+ , Set.spacingCombiningMark
114+ , Set.decimalNumber
115+ , Set.connectorPunctuation
116+ ]
111117
112−toJSHeader :: HeaderArg -> String
118+toJSHeader :: HeaderArg -> Text
113119toJSHeader (HeaderArg n) = toValidFunctionName ("header" <> n)
114120toJSHeader (ReplaceHeaderArg n p)
115− | pn `isPrefixOf` p = pv <> " + \"" <> rp <> "\""
116− | pn `isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
117− | pn `isInfixOf` p = "\"" <> (replace pn ("\" + " <> pv <> " + \"") p)
121+ | pn `T.isPrefixOf` p = pv <> " + \"" <> rp <> "\""
122+ | pn `T.isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
123+ | pn `T.isInfixOf` p = "\"" <> (T.replace pn ("\" + " <> pv <> " + \"") p)
118124 <> "\""
119125 | otherwise = p
120126 where
121127 pv = toValidFunctionName ("header" <> n)
122128 pn = "{" <> n <> "}"
123− rp = replace pn "" p
124− -- Use replace method from Data.Text
125− replace old new = T.unpack
126− . T.replace (T.pack old) (T.pack new)
127− . T.pack
129+ rp = T.replace pn "" p
128130
129−jsSegments :: [Segment] -> String
131+jsSegments :: [Segment] -> Text
130132jsSegments [] = ""
131−jsSegments [x] = "/" ++ segmentToStr x False
132−jsSegments (x:xs) = "/" ++ segmentToStr x True ++ jsSegments xs
133+jsSegments [x] = "/" <> segmentToStr x False
134+jsSegments (x:xs) = "/" <> segmentToStr x True <> jsSegments xs
133135
134−segmentToStr :: Segment -> Bool -> String
136+segmentToStr :: Segment -> Bool -> Text
135137segmentToStr (Segment st ms) notTheEnd =
136− segmentTypeToStr st ++ jsMParams ms ++ if notTheEnd then "" else "'"
138+ segmentTypeToStr st <> jsMParams ms <> if notTheEnd then "" else "'"
137139
138−segmentTypeToStr :: SegmentType -> String
140+segmentTypeToStr :: SegmentType -> Text
139141segmentTypeToStr (Static s) = s
140−segmentTypeToStr (Cap s) = "' + encodeURIComponent(" ++ s ++ ") + '"
142+segmentTypeToStr (Cap s) = "' + encodeURIComponent(" <> s <> ") + '"
141143
142−jsGParams :: String -> [QueryArg] -> String
144+jsGParams :: Text -> [QueryArg] -> Text
143145jsGParams _ [] = ""
144146jsGParams _ [x] = paramToStr x False
145−jsGParams s (x:xs) = paramToStr x True ++ s ++ jsGParams s xs
147+jsGParams s (x:xs) = paramToStr x True <> s <> jsGParams s xs
146148
147−jsParams :: [QueryArg] -> String
149+jsParams :: [QueryArg] -> Text
148150jsParams = jsGParams "&"
149151
150−jsMParams :: [MatrixArg] -> String
152+jsMParams :: [MatrixArg] -> Text
151153jsMParams [] = ""
152−jsMParams xs = ";" ++ jsGParams ";" xs
154+jsMParams xs = ";" <> jsGParams ";" xs
153155
154−paramToStr :: QueryArg -> Bool -> String
156+paramToStr :: QueryArg -> Bool -> Text
155157paramToStr qarg notTheEnd =
156158 case qarg ^. argType of
157159 Normal -> name
158− ++ "=' + encodeURIComponent("
159− ++ name
160− ++ if notTheEnd then ") + '" else ")"
161− Flag -> name ++ "="
160+ <> "=' + encodeURIComponent("
161+ <> name
162+ <> if notTheEnd then ") + '" else ")"
163+ Flag -> name <> "="
162164 List -> name
163− ++ "[]=' + encodeURIComponent("
164− ++ name
165− ++ if notTheEnd then ") + '" else ")"
165+ <> "[]=' + encodeURIComponent("
166+ <> name
167+ <> if notTheEnd then ") + '" else ")"
166168 where name = qarg ^. argName
servant-js/src/Servant/JS/JQuery.hs+18−16
@@ -1,8 +1,10 @@
1+{-#LANGUAGE OverloadedStrings #-}
12module Servant.JS.JQuery where
23
34import Control.Lens
4−import Data.List
55import Data.Monoid
6+import qualified Data.Text as T
7+import Data.Text (Text)
68import Servant.Foreign
79import Servant.JS.Internal
810
@@ -10,19 +12,19 @@ import Servant.JS.Internal
1012-- to make the AJAX calls. Uses 'defCommonGeneratorOptions'
1113-- for the generator options.
1214jquery :: JavaScriptGenerator
13−jquery = concatMap generateJQueryJS
15+jquery = mconcat . map generateJQueryJS
1416
1517-- | Generate javascript functions that use the /jQuery/ library
1618-- to make the AJAX calls. Lets you specify your own 'CommonGeneratorOptions'.
1719jqueryWith :: CommonGeneratorOptions -> JavaScriptGenerator
18−jqueryWith opts = concatMap (generateJQueryJSWith opts)
20+jqueryWith opts = mconcat . map (generateJQueryJSWith opts)
1921
2022-- | js codegen using JQuery using default options
21−generateJQueryJS :: AjaxReq -> String
23+generateJQueryJS :: AjaxReq -> Text
2224generateJQueryJS = generateJQueryJSWith defCommonGeneratorOptions
2325
2426-- | js codegen using JQuery
25−generateJQueryJSWith :: CommonGeneratorOptions -> AjaxReq -> String
27+generateJQueryJSWith :: CommonGeneratorOptions -> AjaxReq -> Text
2628generateJQueryJSWith opts req = "\n" <>
2729 fname <> " = function(" <> argsStr <> ")\n"
2830 <> "{\n"
@@ -36,7 +38,7 @@ generateJQueryJSWith opts req = "\n" <>
3638 <> " });\n"
3739 <> "}\n"
3840
39− where argsStr = intercalate ", " args
41+ where argsStr = T.intercalate ", " args
4042 args = captures
4143 ++ map (view argName) queryparams
4244 ++ body
@@ -67,14 +69,14 @@ generateJQueryJSWith opts req = "\n" <>
6769 reqheaders =
6870 if null hs
6971 then ""
70− else " , headers: { " ++ headersStr ++ " }\n"
72+ else " , headers: { " <> headersStr <> " }\n"
7173
72− where headersStr = intercalate ", " $ map headerStr hs
73− headerStr header = "\"" ++
74− headerArgName header ++
75− "\": " ++ toJSHeader header
74+ where headersStr = T.intercalate ", " $ map headerStr hs
75+ headerStr header = "\"" <>
76+ headerArgName header <>
77+ "\": " <> toJSHeader header
7678
77− namespace = if null (moduleName opts)
79+ namespace = if (moduleName opts) == ""
7880 then "var "
7981 else (moduleName opts) <> "."
8082 fname = namespace <> (functionNameBuilder opts $ req ^. funcName)
@@ -82,13 +84,13 @@ generateJQueryJSWith opts req = "\n" <>
8284 method = req ^. reqMethod
8385 url = if url' == "'" then "'/'" else url'
8486 url' = "'"
85− ++ urlPrefix opts
86− ++ urlArgs
87− ++ queryArgs
87+ <> urlPrefix opts
88+ <> urlArgs
89+ <> queryArgs
8890
8991 urlArgs = jsSegments
9092 $ req ^.. reqUrl.path.traverse
9193
9294 queryArgs = if null queryparams
9395 then ""
94− else " + '?" ++ jsParams queryparams
96+ else " + '?" <> jsParams queryparams
servant-js/src/Servant/JS/Vanilla.hs+18−16
@@ -1,7 +1,9 @@
1+{-#LANGUAGE OverloadedStrings #-}
12module Servant.JS.Vanilla where
23
34import Control.Lens
4−import Data.List
5+import Data.Text (Text)
6+import qualified Data.Text as T
57import Data.Monoid
68import Servant.Foreign
79import Servant.JS.Internal
@@ -10,20 +12,20 @@ import Servant.JS.Internal
1012-- to your API, using /XMLHttpRequest/. Uses 'defCommonGeneratorOptions'
1113-- for the 'CommonGeneratorOptions'.
1214vanillaJS :: JavaScriptGenerator
13−vanillaJS = concatMap generateVanillaJS
15+vanillaJS = mconcat . map generateVanillaJS
1416
1517-- | Generate vanilla javascript functions to make AJAX requests
1618-- to your API, using /XMLHttpRequest/. Lets you specify your
1719-- own options.
1820vanillaJSWith :: CommonGeneratorOptions -> JavaScriptGenerator
19−vanillaJSWith opts = concatMap (generateVanillaJSWith opts)
21+vanillaJSWith opts = mconcat . map (generateVanillaJSWith opts)
2022
2123-- | js codegen using XmlHttpRequest using default generation options
22−generateVanillaJS :: AjaxReq -> String
24+generateVanillaJS :: AjaxReq -> Text
2325generateVanillaJS = generateVanillaJSWith defCommonGeneratorOptions
2426
2527-- | js codegen using XmlHttpRequest
26−generateVanillaJSWith :: CommonGeneratorOptions -> AjaxReq -> String
28+generateVanillaJSWith :: CommonGeneratorOptions -> AjaxReq -> Text
2729generateVanillaJSWith opts req = "\n" <>
2830 fname <> " = function(" <> argsStr <> ")\n"
2931 <> "{\n"
@@ -43,7 +45,7 @@ generateVanillaJSWith opts req = "\n" <>
4345 <> " xhr.send(" <> dataBody <> ");\n"
4446 <> "}\n"
4547
46− where argsStr = intercalate ", " args
48+ where argsStr = T.intercalate ", " args
4749 args = captures
4850 ++ map (view argName) queryparams
4951 ++ body
@@ -74,14 +76,14 @@ generateVanillaJSWith opts req = "\n" <>
7476 reqheaders =
7577 if null hs
7678 then ""
77− else headersStr ++ "\n"
79+ else headersStr <> "\n"
7880
79− where headersStr = intercalate "\n" $ map headerStr hs
80− headerStr header = " xhr.setRequestHeader(\"" ++
81− headerArgName header ++
82− "\", " ++ toJSHeader header ++ ");"
81+ where headersStr = T.intercalate "\n" $ map headerStr hs
82+ headerStr header = " xhr.setRequestHeader(\"" <>
83+ headerArgName header <>
84+ "\", " <> toJSHeader header <> ");"
8385
84− namespace = if null (moduleName opts)
86+ namespace = if moduleName opts == ""
8587 then "var "
8688 else (moduleName opts) <> "."
8789 fname = namespace <> (functionNameBuilder opts $ req ^. funcName)
@@ -89,13 +91,13 @@ generateVanillaJSWith opts req = "\n" <>
8991 method = req ^. reqMethod
9092 url = if url' == "'" then "'/'" else url'
9193 url' = "'"
92− ++ urlPrefix opts
93− ++ urlArgs
94− ++ queryArgs
94+ <> urlPrefix opts
95+ <> urlArgs
96+ <> queryArgs
9597
9698 urlArgs = jsSegments
9799 $ req ^.. reqUrl.path.traverse
98100
99101 queryArgs = if null queryparams
100102 then ""
101− else " + '?" ++ jsParams queryparams
103+ else " + '?" <> jsParams queryparams
servant-js/test/Servant/JSSpec.hs+43−27
@@ -1,16 +1,25 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE DataKinds #-}
23{-# LANGUAGE FlexibleInstances #-}
34{-# LANGUAGE QuasiQuotes #-}
45{-# LANGUAGE ScopedTypeVariables #-}
56{-# LANGUAGE TypeFamilies #-}
67{-# LANGUAGE TypeOperators #-}
8+{-# LANGUAGE OverloadedStrings #-}
79{-# OPTIONS_GHC -fno-warn-orphans #-}
810module Servant.JSSpec where
911
1012import Data.Either (isRight)
13+#if !MIN_VERSION_base(4,8,0)
14+import Data.Monoid ((<>),mconcat)
15+#else
16+import Data.Monoid ((<>))
17+#endif
1118import Data.Proxy
12−import Language.ECMAScript3.Parser (parseFromString)
13−import Test.Hspec
19+import Data.Text (Text)
20+import qualified Data.Text as T
21+import Language.ECMAScript3.Parser (program, parse)
22+import Test.Hspec hiding (shouldContain, shouldNotContain)
1423
1524import Servant.JS
1625import Servant.JS.Internal
@@ -20,22 +29,22 @@ import qualified Servant.JS.JQuery as JQ
2029import qualified Servant.JS.Vanilla as JS
2130import Servant.JSSpec.CustomHeaders
2231
23−type TestAPI = "simple" :> ReqBody '[JSON,FormUrlEncoded] String :> Post '[JSON] Bool
32+type TestAPI = "simple" :> ReqBody '[JSON,FormUrlEncoded] Text :> Post '[JSON] Bool
2433 :<|> "has.extension" :> Get '[FormUrlEncoded,JSON] Bool
2534
2635type TopLevelRawAPI = "something" :> Get '[JSON] Int
2736 :<|> Raw
2837
29−type HeaderHandlingAPI = "something" :> Header "Foo" String
38+type HeaderHandlingAPI = "something" :> Header "Foo" Text
3039 :> Get '[JSON] Int
3140
32−type CustomAuthAPI = "something" :> Authorization "Basic" String
41+type CustomAuthAPI = "something" :> Authorization "Basic" Text
3342 :> Get '[JSON] Int
3443
35−type CustomHeaderAPI = "something" :> MyLovelyHorse String
44+type CustomHeaderAPI = "something" :> MyLovelyHorse Text
3645 :> Get '[JSON] Int
3746
38−type CustomHeaderAPI2 = "something" :> WhatsForDinner String
47+type CustomHeaderAPI2 = "something" :> WhatsForDinner Text
3948 :> Get '[JSON] Int
4049
4150headerHandlingProxy :: Proxy HeaderHandlingAPI
@@ -81,12 +90,18 @@ spec = describe "Servant.JQuery" $ do
8190 axiosSpec
8291 --angularSpec AngularCustom
8392
93+shouldContain :: Text -> Text -> Expectation
94+a `shouldContain` b = shouldSatisfy a (T.isInfixOf b)
95+
96+shouldNotContain :: Text -> Text -> Expectation
97+a `shouldNotContain` b = shouldNotSatisfy a (T.isInfixOf b)
98+
8499axiosSpec :: Spec
85100axiosSpec = describe specLabel $ do
86101 it "should add withCredentials when needed" $ do
87102 let jsText = genJS withCredOpts $ listFromAPI (Proxy :: Proxy TestAPI)
88103 output jsText
89− jsText `shouldContain` ("withCredentials: true")
104+ jsText `shouldContain` "withCredentials: true"
90105 it "should add xsrfCookieName when needed" $ do
91106 let jsText = genJS cookieOpts $ listFromAPI (Proxy :: Proxy TestAPI)
92107 output jsText
@@ -101,79 +116,80 @@ axiosSpec = describe specLabel $ do
101116 withCredOpts = AX.defAxiosOptions { AX.withCredentials = True }
102117 cookieOpts = AX.defAxiosOptions { AX.xsrfCookieName = Just "MyXSRFcookie" }
103118 headerOpts = AX.defAxiosOptions { AX.xsrfHeaderName = Just "MyXSRFheader" }
104− genJS :: AxiosOptions -> [AjaxReq] -> String
105− genJS opts req = concatMap (AX.generateAxiosJS opts) req
119+ genJS :: AxiosOptions -> [AjaxReq] -> Text
120+ genJS opts req = mconcat . map (AX.generateAxiosJS opts) $ req
106121
107122angularSpec :: TestNames -> Spec
108123angularSpec test = describe specLabel $ do
109124 it "should implement a service globally" $ do
110125 let jsText = genJS $ listFromAPI (Proxy :: Proxy TestAPI)
111126 output jsText
112− jsText `shouldContain` (".service('" ++ testName ++ "'")
127+ jsText `shouldContain` (".service('" <> testName <> "'")
113128
114129 it "should depend on $http service globally" $ do
115130 let jsText = genJS $ listFromAPI (Proxy :: Proxy TestAPI)
116131 output jsText
117− jsText `shouldContain` ("('" ++ testName ++ "', function($http) {")
132+ jsText `shouldContain` ("('" <> testName <> "', function($http) {")
118133
119134 it "should not depend on $http service in handlers" $ do
120135 let jsText = genJS $ listFromAPI (Proxy :: Proxy TestAPI)
121136 output jsText
122137 jsText `shouldNotContain` "getsomething($http, "
123138 where
124− specLabel = "AngularJS(" ++ (show test) ++ ")"
139+ specLabel = "AngularJS(" <> (show test) <> ")"
125140 output _ = return ()
126141 testName = "MyService"
127142 ngOpts = NG.defAngularOptions { NG.serviceName = testName }
128143 genJS req = NG.angularService ngOpts req
129144
130−generateJSSpec :: TestNames -> (AjaxReq -> String) -> Spec
145+generateJSSpec :: TestNames -> (AjaxReq -> Text) -> Spec
131146generateJSSpec n gen = describe specLabel $ do
147+ let parseFromText = parse program ""
132148 it "should generate valid javascript" $ do
133− let s = jsForAPI (Proxy :: Proxy TestAPI) (concatMap gen)
134− parseFromString s `shouldSatisfy` isRight
149+ let s = jsForAPI (Proxy :: Proxy TestAPI) (mconcat . map gen)
150+ parseFromText s `shouldSatisfy` isRight
135151
136152 it "should use non-empty function names" $ do
137153 let (_ :<|> topLevel) = javascript (Proxy :: Proxy TopLevelRawAPI)
138154 output $ genJS (topLevel "GET")
139− parseFromString (genJS $ topLevel "GET") `shouldSatisfy` isRight
155+ parseFromText (genJS $ topLevel "GET") `shouldSatisfy` isRight
140156
141157 it "should handle simple HTTP headers" $ do
142158 let jsText = genJS $ javascript headerHandlingProxy
143159 output jsText
144− parseFromString jsText `shouldSatisfy` isRight
160+ parseFromText jsText `shouldSatisfy` isRight
145161 jsText `shouldContain` "headerFoo"
146162 jsText `shouldContain` (header n "Foo" $ "headerFoo")
147163
148164 it "should handle complex HTTP headers" $ do
149165 let jsText = genJS $ javascript customAuthProxy
150166 output jsText
151− parseFromString jsText `shouldSatisfy` isRight
167+ parseFromText jsText `shouldSatisfy` isRight
152168 jsText `shouldContain` "headerAuthorization"
153169 jsText `shouldContain` (header n "Authorization" $ "\"Basic \" + headerAuthorization")
154170
155171 it "should handle complex, custom HTTP headers" $ do
156172 let jsText = genJS $ javascript customHeaderProxy
157173 output jsText
158− parseFromString jsText `shouldSatisfy` isRight
174+ parseFromText jsText `shouldSatisfy` isRight
159175 jsText `shouldContain` "headerXMyLovelyHorse"
160176 jsText `shouldContain` (header n "X-MyLovelyHorse" $ "\"I am good friends with \" + headerXMyLovelyHorse")
161177
162178 it "should handle complex, custom HTTP headers (template replacement)" $ do
163179 let jsText = genJS $ javascript customHeaderProxy2
164180 output jsText
165− parseFromString jsText `shouldSatisfy` isRight
181+ parseFromText jsText `shouldSatisfy` isRight
166182 jsText `shouldContain` "headerXWhatsForDinner"
167183 jsText `shouldContain` (header n "X-WhatsForDinner" $ "\"I would like \" + headerXWhatsForDinner + \" with a cherry on top.\"")
168184
169185 it "can generate the whole javascript code string at once with jsForAPI" $ do
170− let jsStr = jsForAPI (Proxy :: Proxy TestAPI) (concatMap gen)
171− parseFromString jsStr `shouldSatisfy` isRight
186+ let jsStr = jsForAPI (Proxy :: Proxy TestAPI) (mconcat . map gen)
187+ parseFromText jsStr `shouldSatisfy` isRight
172188 where
173− specLabel = "generateJS(" ++ (show n) ++ ")"
189+ specLabel = "generateJS(" <> (show n) <> ")"
174190 output _ = return ()
175191 genJS req = gen req
176− header :: TestNames -> String -> String -> String
192+ header :: TestNames -> Text -> Text -> Text
177193 header v headerName headerValue
178− | v `elem` [Vanilla, VanillaCustom] = "xhr.setRequestHeader(\"" ++ headerName ++ "\", " ++ headerValue ++ ");\n"
179− | otherwise = "headers: { \"" ++ headerName ++ "\": " ++ headerValue ++ " }\n"
194+ | v `elem` [Vanilla, VanillaCustom] = "xhr.setRequestHeader(\"" <> headerName <> "\", " <> headerValue <> ");\n"
195+ | otherwise = "headers: { \"" <> headerName <> "\": " <> headerValue <> " }\n"
servant-js/test/Servant/JSSpec/CustomHeaders.hs+3−1
@@ -5,12 +5,14 @@
55{-# LANGUAGE ScopedTypeVariables #-}
66{-# LANGUAGE TypeFamilies #-}
77{-# LANGUAGE TypeOperators #-}
8+{-# LANGUAGE OverloadedStrings #-}
89
910module Servant.JSSpec.CustomHeaders where
1011
1112import Control.Lens
1213import Data.Monoid
1314import Data.Proxy
15+import Data.Text (pack)
1416import GHC.TypeLits
1517import Servant.JS.Internal
1618
@@ -25,7 +27,7 @@ instance (KnownSymbol sym, HasForeign sublayout)
2527
2628 foreignFor Proxy req = foreignFor (Proxy :: Proxy sublayout) $
2729 req & reqHeaders <>~ [ ReplaceHeaderArg "Authorization" $
28− tokenType (symbolVal (Proxy :: Proxy sym)) ]
30+ tokenType (pack . symbolVal $ (Proxy :: Proxy sym)) ]
2931 where
3032 tokenType t = t <> " {Authorization}"
3133