on master help sign in
Extract common code-generation stuff to servant-foreign
commitce3e4f71932550d6664f85d0d1f772e20dcd7218 authorDenis Redozubov <denis.redozubov@gmail.com> dateMon 21 Sep 2015, 13:31 (11 years ago) parent9f13684
17 files changed+562−428
servant-foreign/CHANGELOG.md+4 servant-foreign/LICENSE+30 servant-foreign/README.md+5 servant-foreign/Setup.hs+2 servant-foreign/servant-foreign.cabal+29 servant-foreign/src/Servant/Foreign.hs+409 servant-js/servant-js.cabal+21−23 servant-js/src/Servant/JS.hs+7−9 servant-js/src/Servant/JS/Angular.hs+1 servant-js/src/Servant/JS/Axios.hs+1 servant-js/src/Servant/JS/Internal.hs+35−380 servant-js/src/Servant/JS/JQuery.hs+1 servant-js/src/Servant/JS/Vanilla.hs+1 servant-js/test/Servant/JSSpec.hs+2−2 servant-js/test/Servant/JSSpec/CustomHeaders.hs+12−14 sources.txt+1 stack.yaml+1
servant-foreign/CHANGELOG.mdnew file+4
@@ -0,0 +1,4 @@
1+0.1
2+-----
3+
4+* Extract javascript-oblivious types and helpers to *servant-foreign*
servant-foreign/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2014, Zalora South East Asia Pte Ltd
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Zalora South East Asia Pte Ltd nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
servant-foreign/README.mdnew file+5
@@ -0,0 +1,5 @@
1+# servant-foreign
2+
3+![servant](https://raw.githubusercontent.com/haskell-servant/servant/master/servant.png)
4+
5+Types and helpers for generating clients for *servant* servers in arbitrary programming languages.
servant-foreign/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
servant-foreign/servant-foreign.cabalnew file+29
@@ -0,0 +1,29 @@
1+name: servant-foreign
2+version: 0.1.0.0
3+synopsis: Abstract all necessary info for client code-generation in arbitrary programming language.
4+description: Consolidates such projects as servant-js and lackey.
5+license: BSD3
6+license-file: LICENSE
7+author: Denis Redozubov
8+maintainer: denis.redozubov@gmail.com
9+copyright: 2015 Denis Redozubov, Alp Mestanogullari
10+category: Web
11+build-type: Simple
12+cabal-version: >=1.10
13+extra-source-files:
14+ CHANGELOG.md
15+ README.md
16+source-repository head
17+ type: git
18+ location: http://github.com/haskell-servant/servant.git
19+
20+library
21+ exposed-modules: Servant.Foreign
22+ build-depends: base >=4.8 && <5
23+ , charset
24+ , lens >= 4
25+ , servant >= 0.5
26+ , text
27+ hs-source-dirs: src
28+ default-language: Haskell2010
29+ ghc-options: -Wall
servant-foreign/src/Servant/Foreign.hsnew file+409
@@ -0,0 +1,409 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE ScopedTypeVariables #-}
7+{-# LANGUAGE TemplateHaskell #-}
8+{-# LANGUAGE TypeFamilies #-}
9+{-# LANGUAGE TypeOperators #-}
10+{-# LANGUAGE UndecidableInstances #-}
11+
12+-- | Generalizes all the data needed to make code generation work with
13+-- arbitrary programming languages.
14+module Servant.Foreign
15+ ( HasForeign(..)
16+ , Segment(..)
17+ , SegmentType(..)
18+ , MatrixArg
19+ , FunctionName
20+ , QueryArg(..)
21+ , HeaderArg(..)
22+ , ArgType(..)
23+ , Req
24+ , CommonGeneratorOptions(..)
25+ , defCommonGeneratorOptions
26+ , toValidFunctionName
27+ , captureArg
28+ , defReq
29+ , concatCase
30+ , snakeCase
31+ , camelCase
32+ -- lenses
33+ , argType
34+ , argName
35+ , isCapture
36+ , funcName
37+ , path
38+ , reqUrl
39+ , reqBody
40+ , reqHeaders
41+ , reqMethod
42+ , segment
43+ , queryStr
44+ -- re-exports
45+ , module Servant.API
46+ ) where
47+
48+#if !MIN_VERSION_base(4,8,0)
49+import Control.Applicative
50+#endif
51+import Control.Lens hiding (List)
52+import Data.Char (toLower, toUpper)
53+import qualified Data.CharSet as Set
54+import qualified Data.CharSet.Unicode.Category as Set
55+import Data.List
56+import Data.Monoid
57+import Data.Proxy
58+import qualified Data.Text as T
59+import GHC.Exts (Constraint)
60+import GHC.TypeLits
61+import Servant.API
62+
63+-- | This structure is used by specific implementations to let you
64+-- customize the output
65+data CommonGeneratorOptions = CommonGeneratorOptions
66+ {
67+ functionNameBuilder :: FunctionName -> String -- ^ function generating function names
68+ , requestBody :: String -- ^ name used when a user want to send the request body (to let you redefine it)
69+ , successCallback :: String -- ^ name of the callback parameter when the request was successful
70+ , errorCallback :: String -- ^ name of the callback parameter when the request reported an error
71+ , moduleName :: String -- ^ namespace on which we define the foreign function (empty mean local var)
72+ , urlPrefix :: String -- ^ a prefix we should add to the Url in the codegen
73+ }
74+
75+-- | Default options.
76+--
77+-- @
78+-- > defCommonGeneratorOptions = CommonGeneratorOptions
79+-- > { functionNameBuilder = camelCase
80+-- > , requestBody = "body"
81+-- > , successCallback = "onSuccess"
82+-- > , errorCallback = "onError"
83+-- > , moduleName = ""
84+-- > , urlPrefix = ""
85+-- > }
86+-- @
87+defCommonGeneratorOptions :: CommonGeneratorOptions
88+defCommonGeneratorOptions = CommonGeneratorOptions
89+ {
90+ functionNameBuilder = camelCase
91+ , requestBody = "body"
92+ , successCallback = "onSuccess"
93+ , errorCallback = "onError"
94+ , moduleName = ""
95+ , urlPrefix = ""
96+ }
97+
98+-- | Function name builder that simply concat each part together
99+concatCase :: FunctionName -> String
100+concatCase = concat
101+
102+-- | Function name builder using the snake_case convention.
103+-- each part is separated by a single underscore character.
104+snakeCase :: FunctionName -> String
105+snakeCase = intercalate "_"
106+
107+-- | Function name builder using the CamelCase convention.
108+-- each part begins with an upper case character.
109+camelCase :: FunctionName -> String
110+camelCase [] = ""
111+camelCase (p:ps) = concat $ p : camelCase' ps
112+ where camelCase' [] = []
113+ camelCase' (r:rs) = capitalize r : camelCase' rs
114+ capitalize [] = []
115+ capitalize (x:xs) = toUpper x : xs
116+
117+type Arg = String
118+
119+data Segment = Segment { _segment :: SegmentType, _matrix :: [MatrixArg] }
120+ deriving (Eq, Show)
121+
122+data SegmentType = Static String -- ^ a static path segment. like "/foo"
123+ | Cap Arg -- ^ a capture. like "/:userid"
124+ deriving (Eq, Show)
125+
126+type Path = [Segment]
127+
128+data ArgType =
129+ Normal
130+ | Flag
131+ | List
132+ deriving (Eq, Show)
133+
134+data QueryArg = QueryArg
135+ { _argName :: Arg
136+ , _argType :: ArgType
137+ } deriving (Eq, Show)
138+
139+data HeaderArg = HeaderArg
140+ { headerArgName :: String
141+ }
142+ | ReplaceHeaderArg
143+ { headerArgName :: String
144+ , headerPattern :: String
145+ } deriving (Eq)
146+
147+
148+-- |
149+instance Show HeaderArg where
150+ show (HeaderArg n) = toValidFunctionName ("header" <> n)
151+ show (ReplaceHeaderArg n p)
152+ | pn `isPrefixOf` p = pv <> " + \"" <> rp <> "\""
153+ | pn `isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
154+ | pn `isInfixOf` p = "\"" <> (replace pn ("\" + " <> pv <> " + \"") p)
155+ <> "\""
156+ | otherwise = p
157+ where
158+ pv = toValidFunctionName ("header" <> n)
159+ pn = "{" <> n <> "}"
160+ rp = replace pn "" p
161+ -- Use replace method from Data.Text
162+ replace old new = T.unpack .
163+ T.replace (T.pack old) (T.pack new) .
164+ T.pack
165+
166+-- | Attempts to reduce the function name provided to that allowed by @'Foreign'@.
167+--
168+-- Here we are making an assumption that js identifiers are common enough.
169+-- https://mathiasbynens.be/notes/javascript-identifiers
170+-- Couldn't work out how to handle zero-width characters.
171+-- TODO: compare it with other generated languages(such as ruby via lackey)
172+-- and generalize.
173+--
174+-- @TODO: specify better default function name, or throw error?
175+toValidFunctionName :: String -> String
176+toValidFunctionName (x:xs) = [setFirstChar x] <> filter remainder xs
177+ where
178+ setFirstChar c = if firstChar c
179+ then c
180+ else '_'
181+ firstChar c = prefixOK c || any (Set.member c) firstLetterOK
182+ remainder c = prefixOK c || any (Set.member c) remainderOK
183+ -- Valid prefixes
184+ prefixOK c = c `elem` ['$','_']
185+ -- Unicode character sets
186+ firstLetterOK = [ Set.lowercaseLetter
187+ , Set.uppercaseLetter
188+ , Set.titlecaseLetter
189+ , Set.modifierLetter
190+ , Set.otherLetter
191+ , Set.letterNumber ]
192+ remainderOK = firstLetterOK <> [ Set.nonSpacingMark
193+ , Set.spacingCombiningMark
194+ , Set.decimalNumber
195+ , Set.connectorPunctuation ]
196+toValidFunctionName [] = "_"
197+
198+type MatrixArg = QueryArg
199+
200+data Url = Url
201+ { _path :: Path
202+ , _queryStr :: [QueryArg]
203+ } deriving (Eq, Show)
204+
205+defUrl :: Url
206+defUrl = Url [] []
207+
208+type FunctionName = [String]
209+type Method = String
210+
211+data Req = Req
212+ { _reqUrl :: Url
213+ , _reqMethod :: Method
214+ , _reqHeaders :: [HeaderArg]
215+ , _reqBody :: Bool
216+ , _funcName :: FunctionName
217+ } deriving (Eq, Show)
218+
219+makeLenses ''QueryArg
220+makeLenses ''Segment
221+makeLenses ''Url
222+makeLenses ''Req
223+
224+isCapture :: Segment -> Bool
225+isCapture (Segment (Cap _) _) = True
226+isCapture _ = False
227+
228+captureArg :: Segment -> Arg
229+captureArg (Segment (Cap s) _) = s
230+captureArg _ = error "captureArg called on non capture"
231+
232+defReq :: Req
233+defReq = Req defUrl "GET" [] False []
234+
235+type family Elem (a :: *) (ls::[*]) :: Constraint where
236+ Elem a '[] = 'False ~ 'True
237+ Elem a (a ': list) = ()
238+ Elem a (b ': list) = Elem a list
239+
240+class HasForeign (layout :: *) where
241+ type Foreign layout :: *
242+ foreignFor :: Proxy layout -> Req -> Foreign layout
243+
244+instance (HasForeign a, HasForeign b)
245+ => HasForeign (a :<|> b) where
246+ type Foreign (a :<|> b) = Foreign a :<|> Foreign b
247+
248+ foreignFor Proxy req =
249+ foreignFor (Proxy :: Proxy a) req
250+ :<|> foreignFor (Proxy :: Proxy b) req
251+
252+instance (KnownSymbol sym, HasForeign sublayout)
253+ => HasForeign (Capture sym a :> sublayout) where
254+ type Foreign (Capture sym a :> sublayout) = Foreign sublayout
255+
256+ foreignFor Proxy req =
257+ foreignFor (Proxy :: Proxy sublayout) $
258+ req & reqUrl.path <>~ [Segment (Cap str) []]
259+ & funcName %~ (++ ["by", str])
260+
261+ where str = symbolVal (Proxy :: Proxy sym)
262+
263+instance Elem JSON list => HasForeign (Delete list a) where
264+ type Foreign (Delete list a) = Req
265+
266+ foreignFor Proxy req =
267+ req & funcName %~ ("delete" :)
268+ & reqMethod .~ "DELETE"
269+
270+instance Elem JSON list => HasForeign (Get list a) where
271+ type Foreign (Get list a) = Req
272+
273+ foreignFor Proxy req =
274+ req & funcName %~ ("get" :)
275+ & reqMethod .~ "GET"
276+
277+instance (KnownSymbol sym, HasForeign sublayout)
278+ => HasForeign (Header sym a :> sublayout) where
279+ type Foreign (Header sym a :> sublayout) = Foreign sublayout
280+
281+ foreignFor Proxy req =
282+ foreignFor subP (req & reqHeaders <>~ [HeaderArg hname])
283+
284+ where hname = symbolVal (Proxy :: Proxy sym)
285+ subP = Proxy :: Proxy sublayout
286+
287+instance Elem JSON list => HasForeign (Post list a) where
288+ type Foreign (Post list a) = Req
289+
290+ foreignFor Proxy req =
291+ req & funcName %~ ("post" :)
292+ & reqMethod .~ "POST"
293+
294+instance Elem JSON list => HasForeign (Put list a) where
295+ type Foreign (Put list a) = Req
296+
297+ foreignFor Proxy req =
298+ req & funcName %~ ("put" :)
299+ & reqMethod .~ "PUT"
300+
301+instance (KnownSymbol sym, HasForeign sublayout)
302+ => HasForeign (QueryParam sym a :> sublayout) where
303+ type Foreign (QueryParam sym a :> sublayout) = Foreign sublayout
304+
305+ foreignFor Proxy req =
306+ foreignFor (Proxy :: Proxy sublayout) $
307+ req & reqUrl.queryStr <>~ [QueryArg str Normal]
308+
309+ where str = symbolVal (Proxy :: Proxy sym)
310+
311+instance (KnownSymbol sym, HasForeign sublayout)
312+ => HasForeign (QueryParams sym a :> sublayout) where
313+ type Foreign (QueryParams sym a :> sublayout) = Foreign sublayout
314+
315+ foreignFor Proxy req =
316+ foreignFor (Proxy :: Proxy sublayout) $
317+ req & reqUrl.queryStr <>~ [QueryArg str List]
318+
319+ where str = symbolVal (Proxy :: Proxy sym)
320+
321+instance (KnownSymbol sym, HasForeign sublayout)
322+ => HasForeign (QueryFlag sym :> sublayout) where
323+ type Foreign (QueryFlag sym :> sublayout) = Foreign sublayout
324+
325+ foreignFor Proxy req =
326+ foreignFor (Proxy :: Proxy sublayout) $
327+ req & reqUrl.queryStr <>~ [QueryArg str Flag]
328+
329+ where str = symbolVal (Proxy :: Proxy sym)
330+
331+instance (KnownSymbol sym, HasForeign sublayout)
332+ => HasForeign (MatrixParam sym a :> sublayout) where
333+ type Foreign (MatrixParam sym a :> sublayout) = Foreign sublayout
334+
335+ foreignFor Proxy req =
336+ foreignFor (Proxy :: Proxy sublayout) $
337+ req & reqUrl.path._last.matrix <>~ [QueryArg strArg Normal]
338+
339+ where str = symbolVal (Proxy :: Proxy sym)
340+ strArg = str ++ "Value"
341+
342+instance (KnownSymbol sym, HasForeign sublayout)
343+ => HasForeign (MatrixParams sym a :> sublayout) where
344+ type Foreign (MatrixParams sym a :> sublayout) = Foreign sublayout
345+
346+ foreignFor Proxy req =
347+ foreignFor (Proxy :: Proxy sublayout) $
348+ req & reqUrl.path._last.matrix <>~ [QueryArg str List]
349+
350+ where str = symbolVal (Proxy :: Proxy sym)
351+
352+instance (KnownSymbol sym, HasForeign sublayout)
353+ => HasForeign (MatrixFlag sym :> sublayout) where
354+ type Foreign (MatrixFlag sym :> sublayout) = Foreign sublayout
355+
356+ foreignFor Proxy req =
357+ foreignFor (Proxy :: Proxy sublayout) $
358+ req & reqUrl.path._last.matrix <>~ [QueryArg str Flag]
359+
360+ where str = symbolVal (Proxy :: Proxy sym)
361+
362+instance HasForeign Raw where
363+ type Foreign Raw = Method -> Req
364+
365+ foreignFor Proxy req method =
366+ req & funcName %~ ((toLower <$> method) :)
367+ & reqMethod .~ method
368+
369+instance (Elem JSON list, HasForeign sublayout) => HasForeign (ReqBody list a :> sublayout) where
370+ type Foreign (ReqBody list a :> sublayout) = Foreign sublayout
371+
372+ foreignFor Proxy req =
373+ foreignFor (Proxy :: Proxy sublayout) $
374+ req & reqBody .~ True
375+
376+instance (KnownSymbol path, HasForeign sublayout)
377+ => HasForeign (path :> sublayout) where
378+ type Foreign (path :> sublayout) = Foreign sublayout
379+
380+ foreignFor Proxy req =
381+ foreignFor (Proxy :: Proxy sublayout) $
382+ req & reqUrl.path <>~ [Segment (Static str) []]
383+ & funcName %~ (++ [str])
384+
385+ where str = map (\c -> if c == '.' then '_' else c) $ symbolVal (Proxy :: Proxy path)
386+
387+instance HasForeign sublayout => HasForeign (RemoteHost :> sublayout) where
388+ type Foreign (RemoteHost :> sublayout) = Foreign sublayout
389+
390+ foreignFor Proxy req =
391+ foreignFor (Proxy :: Proxy sublayout) req
392+
393+instance HasForeign sublayout => HasForeign (IsSecure :> sublayout) where
394+ type Foreign (IsSecure :> sublayout) = Foreign sublayout
395+
396+ foreignFor Proxy req =
397+ foreignFor (Proxy :: Proxy sublayout) req
398+
399+instance HasForeign sublayout => HasForeign (Vault :> sublayout) where
400+ type Foreign (Vault :> sublayout) = Foreign sublayout
401+
402+ foreignFor Proxy req =
403+ foreignFor (Proxy :: Proxy sublayout) req
404+
405+instance HasForeign sublayout => HasForeign (HttpVersion :> sublayout) where
406+ type Foreign (HttpVersion :> sublayout) = Foreign sublayout
407+
408+ foreignFor Proxy req =
409+ foreignFor (Proxy :: Proxy sublayout) req
servant-js/servant-js.cabal+21−23
@@ -37,14 +37,12 @@ library
3737 exposed-modules: Servant.JS
3838 Servant.JS.Angular
3939 Servant.JS.Axios
40+ Servant.JS.Internal
4041 Servant.JS.JQuery
4142 Servant.JS.Vanilla
42− Servant.JS.Internal
4343 build-depends: base >=4.5 && <5
44− , charset
4544 , lens >= 4
46− , servant == 0.5.*
47− , text
45+ , servant-foreign
4846 hs-source-dirs: src
4947 default-language: Haskell2010
5048 ghc-options: -Wall
@@ -59,16 +57,16 @@ executable counter
5957 else
6058 buildable: False
6159
62− build-depends:
63− aeson
64− , base
65− , filepath
66− , servant == 0.5.*
67− , servant-server == 0.5.*
68− , servant-js == 0.5.*
69− , stm
70− , transformers
71− , warp
60+ build-depends: aeson
61+ , base
62+ , filepath
63+ , lens >= 4
64+ , servant
65+ , servant-server
66+ , servant-js
67+ , stm
68+ , transformers
69+ , warp
7270 default-language: Haskell2010
7371
7472test-suite spec
@@ -76,12 +74,12 @@ test-suite spec
7674 hs-source-dirs: test
7775 ghc-options: -Wall
7876 main-is: Spec.hs
79− build-depends:
80− base == 4.*
81− , lens
82− , servant-js
83− , servant
84− , hspec >= 2.1.8
85− , hspec-expectations
86− , language-ecmascript >= 0.16
87− default-language: Haskell2010
77+ build-depends: base == 4.*
78+ , hspec >= 2.1.8
79+ , hspec-expectations
80+ , language-ecmascript >= 0.16
81+ , lens
82+ , lens >= 4
83+ , servant
84+ , servant-js
85+ default-language: Haskell2010
servant-js/src/Servant/JS.hs+7−9
@@ -76,7 +76,7 @@ module Servant.JS
7676 , JavaScriptGenerator
7777
7878 , -- * Options common to all generators
79− CommonGeneratorOptions(..)
79+ CommonGeneratorOptions
8080 , defCommonGeneratorOptions
8181
8282 , -- * Function renamers
@@ -109,13 +109,10 @@ module Servant.JS
109109 , -- * Misc.
110110 listFromAPI
111111 , javascript
112− , HasJS(..)
113112 , GenerateList(..)
114− , AjaxReq
115113 ) where
116114
117115import Data.Proxy
118−import Servant.API
119116import Servant.JS.Angular
120117import Servant.JS.Axios
121118import Servant.JS.Internal
@@ -125,13 +122,13 @@ import Servant.JS.Vanilla
125122-- | Generate the data necessary to generate javascript code
126123-- for all the endpoints of an API, as ':<|>'-separated values
127124-- of type 'AjaxReq'.
128−javascript :: HasJS layout => Proxy layout -> JS layout
129−javascript p = javascriptFor p defReq
125+javascript :: HasForeign layout => Proxy layout -> Foreign layout
126+javascript p = foreignFor p defReq
130127
131128-- | Directly generate all the javascript functions for your API
132129-- from a 'Proxy' for your API type. You can then write it to
133130-- a file or integrate it in a page, for example.
134−jsForAPI :: (HasJS api, GenerateList (JS api))
131+jsForAPI :: (HasForeign api, GenerateList (Foreign api))
135132 => Proxy api -- ^ proxy for your API type
136133 -> JavaScriptGenerator -- ^ js code generator to use (angular, vanilla js, jquery, others)
137134 -> String -- ^ a string that you can embed in your pages or write to a file
@@ -140,7 +137,7 @@ jsForAPI p gen = gen (listFromAPI p)
140137-- | Directly generate all the javascript functions for your API
141138-- from a 'Proxy' for your API type using the given generator
142139-- and write the resulting code to a file at the given path.
143−writeJSForAPI :: (HasJS api, GenerateList (JS api))
140+writeJSForAPI :: (HasForeign api, GenerateList (Foreign api))
144141 => Proxy api -- ^ proxy for your API type
145142 -> JavaScriptGenerator -- ^ js code generator to use (angular, vanilla js, jquery, others)
146143 -> FilePath -- ^ path to the file you want to write the resulting javascript code into
@@ -161,5 +158,6 @@ instance (GenerateList start, GenerateList rest) => GenerateList (start :<|> res
161158
162159-- | Generate the necessary data for JS codegen as a list, each 'AjaxReq'
163160-- describing one endpoint from your API type.
164−listFromAPI :: (HasJS api, GenerateList (JS api)) => Proxy api -> [AjaxReq]
161+listFromAPI :: (HasForeign api, GenerateList (Foreign api)) => Proxy api -> [AjaxReq]
165162listFromAPI p = generateList (javascript p)
163+
servant-js/src/Servant/JS/Angular.hs+1
@@ -3,6 +3,7 @@ module Servant.JS.Angular where
33import Control.Lens
44import Data.List
55import Data.Monoid
6+import Servant.Foreign
67import Servant.JS.Internal
78
89-- | Options specific to the angular code generator
servant-js/src/Servant/JS/Axios.hs+1
@@ -4,6 +4,7 @@ import Control.Lens
44import Data.Char (toLower)
55import Data.List
66import Data.Monoid
7+import Servant.Foreign
78import Servant.JS.Internal
89
910-- | Axios 'configuration' type
servant-js/src/Servant/JS/Internal.hs+35−380
@@ -1,206 +1,43 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE ConstraintKinds #-}
3−{-# LANGUAGE DataKinds #-}
4−{-# LANGUAGE FlexibleContexts #-}
5−{-# LANGUAGE FlexibleInstances #-}
6−{-# LANGUAGE ScopedTypeVariables #-}
7−{-# LANGUAGE TemplateHaskell #-}
8−{-# LANGUAGE TypeFamilies #-}
9−{-# LANGUAGE TypeOperators #-}
10−{-# LANGUAGE UndecidableInstances #-}
11−module Servant.JS.Internal where
1+module Servant.JS.Internal
2+ ( JavaScriptGenerator
3+ , AjaxReq
4+ , jsSegments
5+ , segmentToStr
6+ , segmentTypeToStr
7+ , jsParams
8+ , jsGParams
9+ , jsMParams
10+ , paramToStr
11+ -- re-exports
12+ , (:<|>)(..)
13+ , (:>)
14+ , defReq
15+ , defCommonGeneratorOptions
16+ , reqHeaders
17+ , CommonGeneratorOptions(..)
18+ , HasForeign(..)
19+ , HeaderArg(..)
20+ , concatCase
21+ , snakeCase
22+ , camelCase
23+ , ReqBody
24+ , JSON
25+ , FormUrlEncoded
26+ , Post
27+ , Get
28+ , Raw
29+ , Header
30+ ) where
1231
13−#if !MIN_VERSION_base(4,8,0)
14−import Control.Applicative
15−#endif
1632import Control.Lens hiding (List)
17−import Data.Char (toLower, toUpper)
18−import qualified Data.CharSet as Set
19−import qualified Data.CharSet.Unicode.Category as Set
20−import Data.List
21−import Data.Monoid
22−import Data.Proxy
23−import qualified Data.Text as T
24−import GHC.Exts (Constraint)
25−import GHC.TypeLits
26−import Servant.API
33+import Servant.Foreign
2734
28−-- | this structure is used by JavaScriptGenerator implementations to let you
29−-- customize the output
30−data CommonGeneratorOptions = CommonGeneratorOptions
31− {
32− functionNameBuilder :: FunctionName -> String -- ^ function generating function names
33− , requestBody :: String -- ^ name used when a user want to send the request body (to let you redefine it)
34− , successCallback :: String -- ^ name of the callback parameter when the request was successful
35− , errorCallback :: String -- ^ name of the callback parameter when the request reported an error
36− , moduleName :: String -- ^ namespace on which we define the js function (empty mean local var)
37− , urlPrefix :: String -- ^ a prefix we should add to the Url in the JS codegen
38− }
39−
40−-- | Default options.
41−--
42−-- @
43−-- > defCommonGeneratorOptions = CommonGeneratorOptions
44−-- > { functionNameBuilder = camelCase
45−-- > , requestBody = "body"
46−-- > , successCallback = "onSuccess"
47−-- > , errorCallback = "onError"
48−-- > , moduleName = ""
49−-- > , urlPrefix = ""
50−-- > }
51−-- @
52−defCommonGeneratorOptions :: CommonGeneratorOptions
53−defCommonGeneratorOptions = CommonGeneratorOptions
54− {
55− functionNameBuilder = camelCase
56− , requestBody = "body"
57− , successCallback = "onSuccess"
58− , errorCallback = "onError"
59− , moduleName = ""
60− , urlPrefix = ""
61− }
62−
63−-- | Function name builder that simply concat each part together
64−concatCase :: FunctionName -> String
65−concatCase = concat
66−
67−-- | Function name builder using the snake_case convention.
68−-- each part is separated by a single underscore character.
69−snakeCase :: FunctionName -> String
70−snakeCase = intercalate "_"
71−
72−-- | Function name builder using the CamelCase convention.
73−-- each part begins with an upper case character.
74−camelCase :: FunctionName -> String
75−camelCase [] = ""
76−camelCase (p:ps) = concat $ p : camelCase' ps
77− where camelCase' [] = []
78− camelCase' (r:rs) = capitalize r : camelCase' rs
79− capitalize [] = []
80− capitalize (x:xs) = toUpper x : xs
81−
82−type Arg = String
35+type AjaxReq = Req
8336
8437-- A 'JavascriptGenerator' just takes the data found in the API type
8538-- for each endpoint and generates Javascript code in a String. Several
8639-- generators are available in this package.
87−type JavaScriptGenerator = [AjaxReq] -> String
88−
89−data Segment = Segment { _segment :: SegmentType, _matrix :: [MatrixArg] }
90− deriving (Eq, Show)
91−
92−data SegmentType = Static String -- ^ a static path segment. like "/foo"
93− | Cap Arg -- ^ a capture. like "/:userid"
94− deriving (Eq, Show)
95−
96−type Path = [Segment]
97−
98−data ArgType =
99− Normal
100− | Flag
101− | List
102− deriving (Eq, Show)
103−
104−data QueryArg = QueryArg
105− { _argName :: Arg
106− , _argType :: ArgType
107− } deriving (Eq, Show)
108−
109−data HeaderArg = HeaderArg
110− { headerArgName :: String
111− }
112− | ReplaceHeaderArg
113− { headerArgName :: String
114− , headerPattern :: String
115− } deriving (Eq)
116−
117−instance Show HeaderArg where
118− show (HeaderArg n) = toValidFunctionName ("header" <> n)
119− show (ReplaceHeaderArg n p)
120− | pn `isPrefixOf` p = pv <> " + \"" <> rp <> "\""
121− | pn `isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
122− | pn `isInfixOf` p = "\"" <> (replace pn ("\" + " <> pv <> " + \"") p)
123− <> "\""
124− | otherwise = p
125− where
126− pv = toValidFunctionName ("header" <> n)
127− pn = "{" <> n <> "}"
128− rp = replace pn "" p
129− -- Use replace method from Data.Text
130− replace old new = T.unpack .
131− T.replace (T.pack old) (T.pack new) .
132− T.pack
133−
134−-- | Attempts to reduce the function name provided to that allowed by JS.
135−-- https://mathiasbynens.be/notes/javascript-identifiers
136−-- Couldn't work out how to handle zero-width characters.
137−-- @TODO: specify better default function name, or throw error?
138−toValidFunctionName :: String -> String
139−toValidFunctionName (x:xs) = [setFirstChar x] <> filter remainder xs
140− where
141− setFirstChar c = if firstChar c
142− then c
143− else '_'
144− firstChar c = prefixOK c || any (Set.member c) firstLetterOK
145− remainder c = prefixOK c || any (Set.member c) remainderOK
146− -- Valid prefixes
147− prefixOK c = c `elem` ['$','_']
148− -- Unicode character sets
149− firstLetterOK = [ Set.lowercaseLetter
150− , Set.uppercaseLetter
151− , Set.titlecaseLetter
152− , Set.modifierLetter
153− , Set.otherLetter
154− , Set.letterNumber ]
155− remainderOK = firstLetterOK <> [ Set.nonSpacingMark
156− , Set.spacingCombiningMark
157− , Set.decimalNumber
158− , Set.connectorPunctuation ]
159−toValidFunctionName [] = "_"
160−
161−type MatrixArg = QueryArg
162−
163−data Url = Url
164− { _path :: Path
165− , _queryStr :: [QueryArg]
166− } deriving (Eq, Show)
167−
168−defUrl :: Url
169−defUrl = Url [] []
170−
171−type FunctionName = [String]
172−type Method = String
173−
174−data AjaxReq = AjaxReq
175− { _reqUrl :: Url
176− , _reqMethod :: Method
177− , _reqHeaders :: [HeaderArg]
178− , _reqBody :: Bool
179− , _funcName :: FunctionName
180− } deriving (Eq, Show)
181−
182−makeLenses ''QueryArg
183−makeLenses ''Segment
184−makeLenses ''Url
185−makeLenses ''AjaxReq
186−
187−isCapture :: Segment -> Bool
188−isCapture (Segment (Cap _) _) = True
189−isCapture _ = False
190−
191−hasMatrixArgs :: Segment -> Bool
192−hasMatrixArgs (Segment _ (_:_)) = True
193−hasMatrixArgs _ = False
194−
195−hasArgs :: Segment -> Bool
196−hasArgs s = isCapture s || hasMatrixArgs s
197−
198−matrixArgs :: Segment -> [MatrixArg]
199−matrixArgs (Segment _ ms) = ms
200−
201−captureArg :: Segment -> Arg
202−captureArg (Segment (Cap s) _) = s
203−captureArg _ = error "captureArg called on non capture"
40+type JavaScriptGenerator = [Req] -> String
20441
20542jsSegments :: [Segment] -> String
20643jsSegments [] = ""
@@ -216,8 +53,8 @@ segmentTypeToStr (Static s) = s
21653segmentTypeToStr (Cap s) = "' + encodeURIComponent(" ++ s ++ ") + '"
21754
21855jsGParams :: String -> [QueryArg] -> String
219−jsGParams _ [] = ""
220−jsGParams _ [x] = paramToStr x False
56+jsGParams _ [] = ""
57+jsGParams _ [x] = paramToStr x False
22158jsGParams s (x:xs) = paramToStr x True ++ s ++ jsGParams s xs
22259
22360jsParams :: [QueryArg] -> String
@@ -234,191 +71,9 @@ paramToStr qarg notTheEnd =
23471 ++ "=' + encodeURIComponent("
23572 ++ name
23673 ++ if notTheEnd then ") + '" else ")"
237−
23874 Flag -> name ++ "="
239−
24075 List -> name
24176 ++ "[]=' + encodeURIComponent("
24277 ++ name
24378 ++ if notTheEnd then ") + '" else ")"
244−
24579 where name = qarg ^. argName
246−
247−defReq :: AjaxReq
248−defReq = AjaxReq defUrl "GET" [] False []
249−
250−type family Elem (a :: *) (ls::[*]) :: Constraint where
251− Elem a '[] = 'False ~ 'True
252− Elem a (a ': list) = ()
253− Elem a (b ': list) = Elem a list
254−
255−class HasJS (layout :: *) where
256− type JS layout :: *
257− javascriptFor :: Proxy layout -> AjaxReq -> JS layout
258−
259−instance (HasJS a, HasJS b)
260− => HasJS (a :<|> b) where
261− type JS (a :<|> b) = JS a :<|> JS b
262−
263− javascriptFor Proxy req =
264− javascriptFor (Proxy :: Proxy a) req
265− :<|> javascriptFor (Proxy :: Proxy b) req
266−
267−instance (KnownSymbol sym, HasJS sublayout)
268− => HasJS (Capture sym a :> sublayout) where
269− type JS (Capture sym a :> sublayout) = JS sublayout
270−
271− javascriptFor Proxy req =
272− javascriptFor (Proxy :: Proxy sublayout) $
273− req & reqUrl.path <>~ [Segment (Cap str) []]
274− & funcName %~ (++ ["by", str])
275−
276− where str = symbolVal (Proxy :: Proxy sym)
277−
278−instance Elem JSON list => HasJS (Delete list a) where
279− type JS (Delete list a) = AjaxReq
280−
281− javascriptFor Proxy req =
282− req & funcName %~ ("delete" :)
283− & reqMethod .~ "DELETE"
284−
285−instance Elem JSON list => HasJS (Get list a) where
286− type JS (Get list a) = AjaxReq
287−
288− javascriptFor Proxy req =
289− req & funcName %~ ("get" :)
290− & reqMethod .~ "GET"
291−
292−instance (KnownSymbol sym, HasJS sublayout)
293− => HasJS (Header sym a :> sublayout) where
294− type JS (Header sym a :> sublayout) = JS sublayout
295−
296− javascriptFor Proxy req =
297− javascriptFor subP (req & reqHeaders <>~ [HeaderArg hname])
298−
299− where hname = symbolVal (Proxy :: Proxy sym)
300− subP = Proxy :: Proxy sublayout
301−
302−instance Elem JSON list => HasJS (Post list a) where
303− type JS (Post list a) = AjaxReq
304−
305− javascriptFor Proxy req =
306− req & funcName %~ ("post" :)
307− & reqMethod .~ "POST"
308−
309−instance Elem JSON list => HasJS (Put list a) where
310− type JS (Put list a) = AjaxReq
311−
312− javascriptFor Proxy req =
313− req & funcName %~ ("put" :)
314− & reqMethod .~ "PUT"
315−
316−instance (KnownSymbol sym, HasJS sublayout)
317− => HasJS (QueryParam sym a :> sublayout) where
318− type JS (QueryParam sym a :> sublayout) = JS sublayout
319−
320− javascriptFor Proxy req =
321− javascriptFor (Proxy :: Proxy sublayout) $
322− req & reqUrl.queryStr <>~ [QueryArg str Normal]
323−
324− where str = symbolVal (Proxy :: Proxy sym)
325−
326−instance (KnownSymbol sym, HasJS sublayout)
327− => HasJS (QueryParams sym a :> sublayout) where
328− type JS (QueryParams sym a :> sublayout) = JS sublayout
329−
330− javascriptFor Proxy req =
331− javascriptFor (Proxy :: Proxy sublayout) $
332− req & reqUrl.queryStr <>~ [QueryArg str List]
333−
334− where str = symbolVal (Proxy :: Proxy sym)
335−
336−instance (KnownSymbol sym, HasJS sublayout)
337− => HasJS (QueryFlag sym :> sublayout) where
338− type JS (QueryFlag sym :> sublayout) = JS sublayout
339−
340− javascriptFor Proxy req =
341− javascriptFor (Proxy :: Proxy sublayout) $
342− req & reqUrl.queryStr <>~ [QueryArg str Flag]
343−
344− where str = symbolVal (Proxy :: Proxy sym)
345−
346−instance (KnownSymbol sym, HasJS sublayout)
347− => HasJS (MatrixParam sym a :> sublayout) where
348− type JS (MatrixParam sym a :> sublayout) = JS sublayout
349−
350− javascriptFor Proxy req =
351− javascriptFor (Proxy :: Proxy sublayout) $
352− req & reqUrl.path._last.matrix <>~ [QueryArg strArg Normal]
353−
354− where str = symbolVal (Proxy :: Proxy sym)
355− strArg = str ++ "Value"
356−
357−instance (KnownSymbol sym, HasJS sublayout)
358− => HasJS (MatrixParams sym a :> sublayout) where
359− type JS (MatrixParams sym a :> sublayout) = JS sublayout
360−
361− javascriptFor Proxy req =
362− javascriptFor (Proxy :: Proxy sublayout) $
363− req & reqUrl.path._last.matrix <>~ [QueryArg str List]
364−
365− where str = symbolVal (Proxy :: Proxy sym)
366−
367−instance (KnownSymbol sym, HasJS sublayout)
368− => HasJS (MatrixFlag sym :> sublayout) where
369− type JS (MatrixFlag sym :> sublayout) = JS sublayout
370−
371− javascriptFor Proxy req =
372− javascriptFor (Proxy :: Proxy sublayout) $
373− req & reqUrl.path._last.matrix <>~ [QueryArg str Flag]
374−
375− where str = symbolVal (Proxy :: Proxy sym)
376−
377−instance HasJS Raw where
378− type JS Raw = Method -> AjaxReq
379−
380− javascriptFor Proxy req method =
381− req & funcName %~ ((toLower <$> method) :)
382− & reqMethod .~ method
383−
384−instance (Elem JSON list, HasJS sublayout) => HasJS (ReqBody list a :> sublayout) where
385− type JS (ReqBody list a :> sublayout) = JS sublayout
386−
387− javascriptFor Proxy req =
388− javascriptFor (Proxy :: Proxy sublayout) $
389− req & reqBody .~ True
390−
391−instance (KnownSymbol path, HasJS sublayout)
392− => HasJS (path :> sublayout) where
393− type JS (path :> sublayout) = JS sublayout
394−
395− javascriptFor Proxy req =
396− javascriptFor (Proxy :: Proxy sublayout) $
397− req & reqUrl.path <>~ [Segment (Static str) []]
398− & funcName %~ (++ [str])
399−
400− where str = map (\c -> if c == '.' then '_' else c) $ symbolVal (Proxy :: Proxy path)
401−
402−instance HasJS sublayout => HasJS (RemoteHost :> sublayout) where
403− type JS (RemoteHost :> sublayout) = JS sublayout
404−
405− javascriptFor Proxy req =
406− javascriptFor (Proxy :: Proxy sublayout) req
407−
408−instance HasJS sublayout => HasJS (IsSecure :> sublayout) where
409− type JS (IsSecure :> sublayout) = JS sublayout
410−
411− javascriptFor Proxy req =
412− javascriptFor (Proxy :: Proxy sublayout) req
413−
414−instance HasJS sublayout => HasJS (Vault :> sublayout) where
415− type JS (Vault :> sublayout) = JS sublayout
416−
417− javascriptFor Proxy req =
418− javascriptFor (Proxy :: Proxy sublayout) req
419−
420−instance HasJS sublayout => HasJS (HttpVersion :> sublayout) where
421− type JS (HttpVersion :> sublayout) = JS sublayout
422−
423− javascriptFor Proxy req =
424− javascriptFor (Proxy :: Proxy sublayout) req
servant-js/src/Servant/JS/JQuery.hs+1
@@ -3,6 +3,7 @@ module Servant.JS.JQuery where
33import Control.Lens
44import Data.List
55import Data.Monoid
6+import Servant.Foreign
67import Servant.JS.Internal
78
89-- | Generate javascript functions that use the /jQuery/ library
servant-js/src/Servant/JS/Vanilla.hs+1
@@ -3,6 +3,7 @@ module Servant.JS.Vanilla where
33import Control.Lens
44import Data.List
55import Data.Monoid
6+import Servant.Foreign
67import Servant.JS.Internal
78
89-- | Generate vanilla javascript functions to make AJAX requests
servant-js/test/Servant/JSSpec.hs+2−2
@@ -12,8 +12,8 @@ import Data.Proxy
1212import Language.ECMAScript3.Parser (parseFromString)
1313import Test.Hspec
1414
15−import Servant.API
1615import Servant.JS
16+import Servant.JS.Internal
1717import qualified Servant.JS.Angular as NG
1818import qualified Servant.JS.Axios as AX
1919import qualified Servant.JS.JQuery as JQ
@@ -75,7 +75,7 @@ spec = describe "Servant.JQuery" $ do
7575 generateJSSpec Angular (NG.generateAngularJS NG.defAngularOptions)
7676 generateJSSpec AngularCustom (NG.generateAngularJSWith NG.defAngularOptions customOptions)
7777 generateJSSpec Axios (AX.generateAxiosJS AX.defAxiosOptions)
78− generateJSSpec AxiosCustom (AX.generateAxiosJSWith (AX.defAxiosOptions { withCredentials = True }) customOptions)
78+ generateJSSpec AxiosCustom (AX.generateAxiosJSWith (AX.defAxiosOptions { AX.withCredentials = True }) customOptions)
7979
8080 angularSpec Angular
8181 axiosSpec
servant-js/test/Servant/JSSpec/CustomHeaders.hs+12−14
@@ -12,8 +12,6 @@ import Control.Lens
1212import Data.Monoid
1313import Data.Proxy
1414import GHC.TypeLits
15−import Servant.API
16−import Servant.JS
1715import Servant.JS.Internal
1816
1917-- | This is a hypothetical combinator that fetches an Authorization header.
@@ -21,11 +19,11 @@ import Servant.JS.Internal
2119-- using -- Basic, Digest, whatever.
2220data Authorization (sym :: Symbol) a
2321
24−instance (KnownSymbol sym, HasJS sublayout)
25− => HasJS (Authorization sym a :> sublayout) where
26− type JS (Authorization sym a :> sublayout) = JS sublayout
22+instance (KnownSymbol sym, HasForeign sublayout)
23+ => HasForeign (Authorization sym a :> sublayout) where
24+ type Foreign (Authorization sym a :> sublayout) = Foreign sublayout
2725
28− javascriptFor Proxy req = javascriptFor (Proxy :: Proxy sublayout) $
26+ foreignFor Proxy req = foreignFor (Proxy :: Proxy sublayout) $
2927 req & reqHeaders <>~ [ ReplaceHeaderArg "Authorization" $
3028 tokenType (symbolVal (Proxy :: Proxy sym)) ]
3129 where
@@ -34,11 +32,11 @@ instance (KnownSymbol sym, HasJS sublayout)
3432-- | This is a combinator that fetches an X-MyLovelyHorse header.
3533data MyLovelyHorse a
3634
37−instance (HasJS sublayout)
38− => HasJS (MyLovelyHorse a :> sublayout) where
39− type JS (MyLovelyHorse a :> sublayout) = JS sublayout
35+instance (HasForeign sublayout)
36+ => HasForeign (MyLovelyHorse a :> sublayout) where
37+ type Foreign (MyLovelyHorse a :> sublayout) = Foreign sublayout
4038
41− javascriptFor Proxy req = javascriptFor (Proxy :: Proxy sublayout) $
39+ foreignFor Proxy req = foreignFor (Proxy :: Proxy sublayout) $
4240 req & reqHeaders <>~ [ ReplaceHeaderArg "X-MyLovelyHorse" tpl ]
4341 where
4442 tpl = "I am good friends with {X-MyLovelyHorse}"
@@ -46,11 +44,11 @@ instance (HasJS sublayout)
4644-- | This is a combinator that fetches an X-WhatsForDinner header.
4745data WhatsForDinner a
4846
49−instance (HasJS sublayout)
50− => HasJS (WhatsForDinner a :> sublayout) where
51− type JS (WhatsForDinner a :> sublayout) = JS sublayout
47+instance (HasForeign sublayout)
48+ => HasForeign (WhatsForDinner a :> sublayout) where
49+ type Foreign (WhatsForDinner a :> sublayout) = Foreign sublayout
5250
53− javascriptFor Proxy req = javascriptFor (Proxy :: Proxy sublayout) $
51+ foreignFor Proxy req = foreignFor (Proxy :: Proxy sublayout) $
5452 req & reqHeaders <>~ [ ReplaceHeaderArg "X-WhatsForDinner" tpl ]
5553 where
5654 tpl = "I would like {X-WhatsForDinner} with a cherry on top."
sources.txt+1
@@ -2,6 +2,7 @@ servant
22servant-cassava
33servant-client
44servant-docs
5+servant-foreign
56servant-js
67servant-server
78servant-examples
stack.yaml+1
@@ -8,6 +8,7 @@ packages:
88- servant-client/
99- servant-docs/
1010- servant-examples/
11+- servant-foreign/
1112- servant-js/
1213- servant-lucid/
1314- servant-mock/