git.iko.soy/typeable/servant
on master help sign in
Merge pull request #232 from dredozubov/servant-foreign

Common code generation types and helpers extracted to servant-foreign

commita1dc0561a332aae3309d569df9ca23e518097ea5 authorJulian Arni <jkarni@gmail.com> dateWed 23 Sep 2015, 14:41 (11 years ago) parente1ce5147b58f04
18 files changed+530−386
servant-foreign/CHANGELOG.md+4 servant-foreign/LICENSE+30 servant-foreign/README.md+5 servant-foreign/Setup.hs+2 servant-foreign/servant-foreign.cabal+34 servant-foreign/src/Servant/Foreign.hs+323 servant-js/servant-js.cabal+26−25 servant-js/src/Servant/JS.hs+7−9 servant-js/src/Servant/JS/Angular.hs+2−1 servant-js/src/Servant/JS/Axios.hs+2−1 servant-js/src/Servant/JS/Internal.hs+74−332 servant-js/src/Servant/JS/JQuery.hs+2−1 servant-js/src/Servant/JS/Vanilla.hs+2−1 servant-js/test/Servant/JSSpec.hs+2−2 servant-js/test/Servant/JSSpec/CustomHeaders.hs+12−14 sources.txt+1 stack-ghc-7.8.4.yaml+1 stack.yaml+1
servant-foreign/CHANGELOG.mdnew file+4
@@ -0,0 +1,4 @@
1+0.5
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+34
@@ -0,0 +1,34 @@
1+name: servant-foreign
2+version: 0.5
3+synopsis: Helpers for generating clients for servant APIs in any programming language
4+description:
5+ Helper types and functions for generating client functions for servant APIs in any programming language
6+ .
7+ This package provides types and functions that collect all the data needed to generate client functions in the programming language of your choice. This effectively means you only have to write the code that "pretty-prints" this data as some code in your target language.
8+ .
9+ See the servant-js package for an example
10+ .
11+ <https://github.com/haskell-servant/servant/blob/master/servant-foreign/CHANGELOG.md CHANGELOG>
12+license: BSD3
13+license-file: LICENSE
14+author: Denis Redozubov
15+maintainer: denis.redozubov@gmail.com
16+copyright: 2015 Denis Redozubov, Alp Mestanogullari
17+category: Web
18+build-type: Simple
19+cabal-version: >=1.10
20+extra-source-files:
21+ CHANGELOG.md
22+ README.md
23+source-repository head
24+ type: git
25+ location: http://github.com/haskell-servant/servant.git
26+
27+library
28+ exposed-modules: Servant.Foreign
29+ build-depends: base == 4.*
30+ , lens == 4.*
31+ , servant == 0.5.*
32+ hs-source-dirs: src
33+ default-language: Haskell2010
34+ ghc-options: -Wall
servant-foreign/src/Servant/Foreign.hsnew file+323
@@ -0,0 +1,323 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
7+{-# LANGUAGE NullaryTypeClasses #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TemplateHaskell #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE TypeOperators #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+
14+-- | Generalizes all the data needed to make code generation work with
15+-- arbitrary programming languages.
16+module Servant.Foreign
17+ ( HasForeign(..)
18+ , Segment(..)
19+ , SegmentType(..)
20+ , MatrixArg
21+ , FunctionName
22+ , QueryArg(..)
23+ , HeaderArg(..)
24+ , ArgType(..)
25+ , Req
26+ , captureArg
27+ , defReq
28+ , concatCase
29+ , snakeCase
30+ , camelCase
31+ -- lenses
32+ , argType
33+ , argName
34+ , isCapture
35+ , funcName
36+ , path
37+ , reqUrl
38+ , reqBody
39+ , reqHeaders
40+ , reqMethod
41+ , segment
42+ , queryStr
43+ -- re-exports
44+ , module Servant.API
45+ ) where
46+
47+#if !MIN_VERSION_base(4,8,0)
48+import Control.Applicative
49+#endif
50+import Control.Lens hiding (List)
51+import Data.Char (toLower, toUpper)
52+import Data.List
53+import Data.Proxy
54+import GHC.Exts (Constraint)
55+import GHC.TypeLits
56+import Servant.API
57+
58+-- | Function name builder that simply concat each part together
59+concatCase :: FunctionName -> String
60+concatCase = concat
61+
62+-- | Function name builder using the snake_case convention.
63+-- each part is separated by a single underscore character.
64+snakeCase :: FunctionName -> String
65+snakeCase = intercalate "_"
66+
67+-- | Function name builder using the CamelCase convention.
68+-- each part begins with an upper case character.
69+camelCase :: FunctionName -> String
70+camelCase [] = ""
71+camelCase (p:ps) = concat $ p : camelCase' ps
72+ where camelCase' [] = []
73+ camelCase' (r:rs) = capitalize r : camelCase' rs
74+ capitalize [] = []
75+ capitalize (x:xs) = toUpper x : xs
76+
77+type Arg = String
78+
79+data Segment = Segment { _segment :: SegmentType, _matrix :: [MatrixArg] }
80+ deriving (Eq, Show)
81+
82+data SegmentType = Static String -- ^ a static path segment. like "/foo"
83+ | Cap Arg -- ^ a capture. like "/:userid"
84+ deriving (Eq, Show)
85+
86+type Path = [Segment]
87+
88+data ArgType =
89+ Normal
90+ | Flag
91+ | List
92+ deriving (Eq, Show)
93+
94+data QueryArg = QueryArg
95+ { _argName :: Arg
96+ , _argType :: ArgType
97+ } deriving (Eq, Show)
98+
99+data HeaderArg = HeaderArg
100+ { headerArgName :: String
101+ }
102+ | ReplaceHeaderArg
103+ { headerArgName :: String
104+ , headerPattern :: String
105+ } deriving (Eq, Show)
106+
107+
108+type MatrixArg = QueryArg
109+
110+data Url = Url
111+ { _path :: Path
112+ , _queryStr :: [QueryArg]
113+ } deriving (Eq, Show)
114+
115+defUrl :: Url
116+defUrl = Url [] []
117+
118+type FunctionName = [String]
119+type Method = String
120+
121+data Req = Req
122+ { _reqUrl :: Url
123+ , _reqMethod :: Method
124+ , _reqHeaders :: [HeaderArg]
125+ , _reqBody :: Bool
126+ , _funcName :: FunctionName
127+ } deriving (Eq, Show)
128+
129+makeLenses ''QueryArg
130+makeLenses ''Segment
131+makeLenses ''Url
132+makeLenses ''Req
133+
134+isCapture :: Segment -> Bool
135+isCapture (Segment (Cap _) _) = True
136+isCapture _ = False
137+
138+captureArg :: Segment -> Arg
139+captureArg (Segment (Cap s) _) = s
140+captureArg _ = error "captureArg called on non capture"
141+
142+defReq :: Req
143+defReq = Req defUrl "GET" [] False []
144+
145+-- | To be used exclusively as a "negative" return type/constraint
146+-- by @'Elem`@ type family.
147+class NotFound
148+
149+type family Elem (a :: *) (ls::[*]) :: Constraint where
150+ Elem a '[] = NotFound
151+ Elem a (a ': list) = ()
152+ Elem a (b ': list) = Elem a list
153+
154+class HasForeign (layout :: *) where
155+ type Foreign layout :: *
156+ foreignFor :: Proxy layout -> Req -> Foreign layout
157+
158+instance (HasForeign a, HasForeign b)
159+ => HasForeign (a :<|> b) where
160+ type Foreign (a :<|> b) = Foreign a :<|> Foreign b
161+
162+ foreignFor Proxy req =
163+ foreignFor (Proxy :: Proxy a) req
164+ :<|> foreignFor (Proxy :: Proxy b) req
165+
166+instance (KnownSymbol sym, HasForeign sublayout)
167+ => HasForeign (Capture sym a :> sublayout) where
168+ type Foreign (Capture sym a :> sublayout) = Foreign sublayout
169+
170+ foreignFor Proxy req =
171+ foreignFor (Proxy :: Proxy sublayout) $
172+ req & reqUrl.path <>~ [Segment (Cap str) []]
173+ & funcName %~ (++ ["by", str])
174+
175+ where str = symbolVal (Proxy :: Proxy sym)
176+
177+instance Elem JSON list => HasForeign (Delete list a) where
178+ type Foreign (Delete list a) = Req
179+
180+ foreignFor Proxy req =
181+ req & funcName %~ ("delete" :)
182+ & reqMethod .~ "DELETE"
183+
184+instance Elem JSON list => HasForeign (Get list a) where
185+ type Foreign (Get list a) = Req
186+
187+ foreignFor Proxy req =
188+ req & funcName %~ ("get" :)
189+ & reqMethod .~ "GET"
190+
191+instance (KnownSymbol sym, HasForeign sublayout)
192+ => HasForeign (Header sym a :> sublayout) where
193+ type Foreign (Header sym a :> sublayout) = Foreign sublayout
194+
195+ foreignFor Proxy req =
196+ foreignFor subP (req & reqHeaders <>~ [HeaderArg hname])
197+
198+ where hname = symbolVal (Proxy :: Proxy sym)
199+ subP = Proxy :: Proxy sublayout
200+
201+instance Elem JSON list => HasForeign (Post list a) where
202+ type Foreign (Post list a) = Req
203+
204+ foreignFor Proxy req =
205+ req & funcName %~ ("post" :)
206+ & reqMethod .~ "POST"
207+
208+instance Elem JSON list => HasForeign (Put list a) where
209+ type Foreign (Put list a) = Req
210+
211+ foreignFor Proxy req =
212+ req & funcName %~ ("put" :)
213+ & reqMethod .~ "PUT"
214+
215+instance (KnownSymbol sym, HasForeign sublayout)
216+ => HasForeign (QueryParam sym a :> sublayout) where
217+ type Foreign (QueryParam sym a :> sublayout) = Foreign sublayout
218+
219+ foreignFor Proxy req =
220+ foreignFor (Proxy :: Proxy sublayout) $
221+ req & reqUrl.queryStr <>~ [QueryArg str Normal]
222+
223+ where str = symbolVal (Proxy :: Proxy sym)
224+
225+instance (KnownSymbol sym, HasForeign sublayout)
226+ => HasForeign (QueryParams sym a :> sublayout) where
227+ type Foreign (QueryParams sym a :> sublayout) = Foreign sublayout
228+
229+ foreignFor Proxy req =
230+ foreignFor (Proxy :: Proxy sublayout) $
231+ req & reqUrl.queryStr <>~ [QueryArg str List]
232+
233+ where str = symbolVal (Proxy :: Proxy sym)
234+
235+instance (KnownSymbol sym, HasForeign sublayout)
236+ => HasForeign (QueryFlag sym :> sublayout) where
237+ type Foreign (QueryFlag sym :> sublayout) = Foreign sublayout
238+
239+ foreignFor Proxy req =
240+ foreignFor (Proxy :: Proxy sublayout) $
241+ req & reqUrl.queryStr <>~ [QueryArg str Flag]
242+
243+ where str = symbolVal (Proxy :: Proxy sym)
244+
245+instance (KnownSymbol sym, HasForeign sublayout)
246+ => HasForeign (MatrixParam sym a :> sublayout) where
247+ type Foreign (MatrixParam sym a :> sublayout) = Foreign sublayout
248+
249+ foreignFor Proxy req =
250+ foreignFor (Proxy :: Proxy sublayout) $
251+ req & reqUrl.path._last.matrix <>~ [QueryArg strArg Normal]
252+
253+ where str = symbolVal (Proxy :: Proxy sym)
254+ strArg = str ++ "Value"
255+
256+instance (KnownSymbol sym, HasForeign sublayout)
257+ => HasForeign (MatrixParams sym a :> sublayout) where
258+ type Foreign (MatrixParams sym a :> sublayout) = Foreign sublayout
259+
260+ foreignFor Proxy req =
261+ foreignFor (Proxy :: Proxy sublayout) $
262+ req & reqUrl.path._last.matrix <>~ [QueryArg str List]
263+
264+ where str = symbolVal (Proxy :: Proxy sym)
265+
266+instance (KnownSymbol sym, HasForeign sublayout)
267+ => HasForeign (MatrixFlag sym :> sublayout) where
268+ type Foreign (MatrixFlag sym :> sublayout) = Foreign sublayout
269+
270+ foreignFor Proxy req =
271+ foreignFor (Proxy :: Proxy sublayout) $
272+ req & reqUrl.path._last.matrix <>~ [QueryArg str Flag]
273+
274+ where str = symbolVal (Proxy :: Proxy sym)
275+
276+instance HasForeign Raw where
277+ type Foreign Raw = Method -> Req
278+
279+ foreignFor Proxy req method =
280+ req & funcName %~ ((toLower <$> method) :)
281+ & reqMethod .~ method
282+
283+instance (Elem JSON list, HasForeign sublayout) => HasForeign (ReqBody list a :> sublayout) where
284+ type Foreign (ReqBody list a :> sublayout) = Foreign sublayout
285+
286+ foreignFor Proxy req =
287+ foreignFor (Proxy :: Proxy sublayout) $
288+ req & reqBody .~ True
289+
290+instance (KnownSymbol path, HasForeign sublayout)
291+ => HasForeign (path :> sublayout) where
292+ type Foreign (path :> sublayout) = Foreign sublayout
293+
294+ foreignFor Proxy req =
295+ foreignFor (Proxy :: Proxy sublayout) $
296+ req & reqUrl.path <>~ [Segment (Static str) []]
297+ & funcName %~ (++ [str])
298+
299+ where str = map (\c -> if c == '.' then '_' else c) $ symbolVal (Proxy :: Proxy path)
300+
301+instance HasForeign sublayout => HasForeign (RemoteHost :> sublayout) where
302+ type Foreign (RemoteHost :> sublayout) = Foreign sublayout
303+
304+ foreignFor Proxy req =
305+ foreignFor (Proxy :: Proxy sublayout) req
306+
307+instance HasForeign sublayout => HasForeign (IsSecure :> sublayout) where
308+ type Foreign (IsSecure :> sublayout) = Foreign sublayout
309+
310+ foreignFor Proxy req =
311+ foreignFor (Proxy :: Proxy sublayout) req
312+
313+instance HasForeign sublayout => HasForeign (Vault :> sublayout) where
314+ type Foreign (Vault :> sublayout) = Foreign sublayout
315+
316+ foreignFor Proxy req =
317+ foreignFor (Proxy :: Proxy sublayout) req
318+
319+instance HasForeign sublayout => HasForeign (HttpVersion :> sublayout) where
320+ type Foreign (HttpVersion :> sublayout) = Foreign sublayout
321+
322+ foreignFor Proxy req =
323+ foreignFor (Proxy :: Proxy sublayout) req
servant-js/servant-js.cabal+26−25
@@ -37,14 +37,15 @@ 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
43− build-depends: base >=4.5 && <5
44− , charset
45− , lens >= 4
46− , servant == 0.5.*
47− , text
43+ build-depends: base >= 4.5 && <5
44+ , charset >= 0.3
45+ , lens >= 4
46+ , servant-foreign == 0.5.*
47+ , text >= 1.2 && < 1.3
48+
4849 hs-source-dirs: src
4950 default-language: Haskell2010
5051 ghc-options: -Wall
@@ -59,16 +60,16 @@ executable counter
5960 else
6061 buildable: False
6162
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
63+ build-depends: base >= 4.7 && < 5
64+ , aeson >= 0.7 && < 0.11
65+ , filepath >= 1
66+ , lens >= 4
67+ , servant == 0.5.*
68+ , servant-server == 0.5.*
69+ , servant-js
70+ , stm
71+ , transformers
72+ , warp
7273 default-language: Haskell2010
7374
7475test-suite spec
@@ -76,12 +77,12 @@ test-suite spec
7677 hs-source-dirs: test
7778 ghc-options: -Wall
7879 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
80+ build-depends: base
81+ , hspec >= 2.1.8
82+ , hspec-expectations
83+ , language-ecmascript >= 0.16
84+ , lens
85+ , servant
86+ , servant-js
87+ , text
88+ 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+2−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
@@ -108,7 +109,7 @@ generateAngularJSWith ngOptions opts req = "\n" <>
108109 where headersStr = intercalate ", " $ map headerStr hs
109110 headerStr header = "\"" ++
110111 headerArgName header ++
111− "\": " ++ show header
112+ "\": " ++ toJSHeader header
112113
113114 namespace =
114115 if hasService
servant-js/src/Servant/JS/Axios.hs+2−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
@@ -103,7 +104,7 @@ generateAxiosJSWith aopts opts req = "\n" <>
103104 where headersStr = intercalate ", " $ map headerStr hs
104105 headerStr header = "\"" ++
105106 headerArgName header ++
106− "\": " ++ show header
107+ "\": " ++ toJSHeader header
107108
108109 namespace =
109110 if hasNoModule
servant-js/src/Servant/JS/Internal.hs+74−332
@@ -1,31 +1,52 @@
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
12−
13−#if !MIN_VERSION_base(4,8,0)
14−import Control.Applicative
15−#endif
16−import Control.Lens hiding (List)
17−import Data.Char (toLower, toUpper)
1+module Servant.JS.Internal
2+ ( JavaScriptGenerator
3+ , CommonGeneratorOptions(..)
4+ , defCommonGeneratorOptions
5+ , AjaxReq
6+ , jsSegments
7+ , segmentToStr
8+ , segmentTypeToStr
9+ , jsParams
10+ , jsGParams
11+ , jsMParams
12+ , paramToStr
13+ , toValidFunctionName
14+ , toJSHeader
15+ -- re-exports
16+ , (:<|>)(..)
17+ , (:>)
18+ , defReq
19+ , reqHeaders
20+ , HasForeign(..)
21+ , HeaderArg(..)
22+ , concatCase
23+ , snakeCase
24+ , camelCase
25+ , ReqBody
26+ , JSON
27+ , FormUrlEncoded
28+ , Post
29+ , Get
30+ , Raw
31+ , Header
32+ ) where
33+
34+import Control.Lens hiding (List)
1835import qualified Data.CharSet as Set
1936import qualified Data.CharSet.Unicode.Category as Set
20−import Data.List
21−import Data.Monoid
22−import Data.Proxy
37+import Data.List
38+import Data.Monoid
2339import qualified Data.Text as T
24−import GHC.Exts (Constraint)
25−import GHC.TypeLits
26−import Servant.API
40+import Servant.Foreign
41+
42+type AjaxReq = Req
43+
44+-- A 'JavascriptGenerator' just takes the data found in the API type
45+-- for each endpoint and generates Javascript code in a String. Several
46+-- generators are available in this package.
47+type JavaScriptGenerator = [Req] -> String
2748
28−-- | this structure is used by JavaScriptGenerator implementations to let you
49+-- | This structure is used by specific implementations to let you
2950-- customize the output
3051data CommonGeneratorOptions = CommonGeneratorOptions
3152 {
@@ -33,8 +54,8 @@ data CommonGeneratorOptions = CommonGeneratorOptions
3354 , requestBody :: String -- ^ name used when a user want to send the request body (to let you redefine it)
3455 , successCallback :: String -- ^ name of the callback parameter when the request was successful
3556 , 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
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
3859 }
3960
4061-- | Default options.
@@ -60,87 +81,16 @@ defCommonGeneratorOptions = CommonGeneratorOptions
6081 , urlPrefix = ""
6182 }
6283
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
83−
84−-- A 'JavascriptGenerator' just takes the data found in the API type
85−-- for each endpoint and generates Javascript code in a String. Several
86−-- 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.
84+-- | Attempts to reduce the function name provided to that allowed by @'Foreign'@.
85+--
13586-- https://mathiasbynens.be/notes/javascript-identifiers
13687-- Couldn't work out how to handle zero-width characters.
88+--
13789-- @TODO: specify better default function name, or throw error?
13890toValidFunctionName :: String -> String
13991toValidFunctionName (x:xs) = [setFirstChar x] <> filter remainder xs
14092 where
141− setFirstChar c = if firstChar c
142− then c
143− else '_'
93+ setFirstChar c = if firstChar c then c else '_'
14494 firstChar c = prefixOK c || any (Set.member c) firstLetterOK
14595 remainder c = prefixOK c || any (Set.member c) remainderOK
14696 -- Valid prefixes
@@ -152,55 +102,29 @@ toValidFunctionName (x:xs) = [setFirstChar x] <> filter remainder xs
152102 , Set.modifierLetter
153103 , Set.otherLetter
154104 , Set.letterNumber ]
155− remainderOK = firstLetterOK <> [ Set.nonSpacingMark
156− , Set.spacingCombiningMark
157− , Set.decimalNumber
158− , Set.connectorPunctuation ]
105+ remainderOK = firstLetterOK
106+ <> [ Set.nonSpacingMark
107+ , Set.spacingCombiningMark
108+ , Set.decimalNumber
109+ , Set.connectorPunctuation ]
159110toValidFunctionName [] = "_"
160111
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"
112+toJSHeader :: HeaderArg -> String
113+toJSHeader (HeaderArg n) = toValidFunctionName ("header" <> n)
114+toJSHeader (ReplaceHeaderArg n p)
115+ | pn `isPrefixOf` p = pv <> " + \"" <> rp <> "\""
116+ | pn `isSuffixOf` p = "\"" <> rp <> "\" + " <> pv
117+ | pn `isInfixOf` p = "\"" <> (replace pn ("\" + " <> pv <> " + \"") p)
118+ <> "\""
119+ | otherwise = p
120+ where
121+ pv = toValidFunctionName ("header" <> n)
122+ 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
204128
205129jsSegments :: [Segment] -> String
206130jsSegments [] = ""
@@ -216,8 +140,8 @@ segmentTypeToStr (Static s) = s
216140segmentTypeToStr (Cap s) = "' + encodeURIComponent(" ++ s ++ ") + '"
217141
218142jsGParams :: String -> [QueryArg] -> String
219−jsGParams _ [] = ""
220−jsGParams _ [x] = paramToStr x False
143+jsGParams _ [] = ""
144+jsGParams _ [x] = paramToStr x False
221145jsGParams s (x:xs) = paramToStr x True ++ s ++ jsGParams s xs
222146
223147jsParams :: [QueryArg] -> String
@@ -234,191 +158,9 @@ paramToStr qarg notTheEnd =
234158 ++ "=' + encodeURIComponent("
235159 ++ name
236160 ++ if notTheEnd then ") + '" else ")"
237−
238161 Flag -> name ++ "="
239−
240162 List -> name
241163 ++ "[]=' + encodeURIComponent("
242164 ++ name
243165 ++ if notTheEnd then ") + '" else ")"
244−
245166 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+2−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
@@ -71,7 +72,7 @@ generateJQueryJSWith opts req = "\n" <>
7172 where headersStr = intercalate ", " $ map headerStr hs
7273 headerStr header = "\"" ++
7374 headerArgName header ++
74− "\": " ++ show header
75+ "\": " ++ toJSHeader header
7576
7677 namespace = if null (moduleName opts)
7778 then "var "
servant-js/src/Servant/JS/Vanilla.hs+2−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
@@ -78,7 +79,7 @@ generateVanillaJSWith opts req = "\n" <>
7879 where headersStr = intercalate "\n" $ map headerStr hs
7980 headerStr header = " xhr.setRequestHeader(\"" ++
8081 headerArgName header ++
81− "\", " ++ show header ++ ");"
82+ "\", " ++ toJSHeader header ++ ");"
8283
8384 namespace = if null (moduleName opts)
8485 then "var "
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-ghc-7.8.4.yaml+1
@@ -6,6 +6,7 @@ packages:
66- servant-client/
77- servant-lucid/
88- servant-mock/
9+- servant-foreign/
910- servant-js/
1011- servant/
1112- servant-server/
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/