on master help sign in
Merge pull request #345 from haskell-servant/fizruk/type-level-#305

Servant.API.TypeLevel

commit555b60ea06dcdbb69f8823d982d2639746803ccd authorOleg Grenrus <oleg.grenrus@iki.fi> dateMon 16 Jan 2017, 14:53 (10 years ago) parent48014f4c7c6c05
6 files changed+276−80
servant-docs/src/Servant/Docs/Internal.hs+1−11 servant-foreign/src/Servant/Foreign/Internal.hs+1−10 servant/CHANGELOG.md+6 servant/servant.cabal+1 servant/src/Servant/API/TypeLevel.hs+264 servant/src/Servant/Utils/Links.hs+3−59
servant-docs/src/Servant/Docs/Internal.hs+1−11
@@ -37,12 +37,11 @@ import Data.Ord (comparing)
3737import Data.Proxy (Proxy(Proxy))
3838import Data.String.Conversions (cs)
3939import Data.Text (Text, unpack)
40−import GHC.Exts (Constraint)
4140import GHC.Generics
4241import GHC.TypeLits
4342import Servant.API
4443import Servant.API.ContentTypes
45−import Servant.Utils.Links
44+import Servant.API.TypeLevel
4645
4746import qualified Data.HashMap.Strict as HM
4847import qualified Data.Text as T
@@ -306,15 +305,6 @@ docs p = docsWithOptions p defaultDocOptions
306305docsWithOptions :: HasDocs api => Proxy api -> DocOptions -> API
307306docsWithOptions p = docsFor p (defEndpoint, defAction)
308307
309−-- | Closed type family, check if endpoint is exactly within API.
310−
311−-- We aren't sure what affects how an Endpoint is built up, so we require an
312−-- exact match.
313−type family IsIn (endpoint :: *) (api :: *) :: Constraint where
314− IsIn e (sa :<|> sb) = Or (IsIn e sa) (IsIn e sb)
315− IsIn (e :> sa) (e :> sb) = IsIn sa sb
316− IsIn e e = ()
317−
318308-- | Create an 'ExtraInfo' that is guaranteed to be within the given API layout.
319309--
320310-- The safety here is to ensure that you only add custom documentation to an
servant-foreign/src/Servant/Foreign/Internal.hs+1−10
@@ -16,11 +16,11 @@ import Data.Proxy
1616import Data.String
1717import Data.Text
1818import Data.Text.Encoding (decodeUtf8)
19−import GHC.Exts (Constraint)
2019import GHC.TypeLits
2120import qualified Network.HTTP.Types as HTTP
2221import Prelude hiding (concat)
2322import Servant.API
23+import Servant.API.TypeLevel
2424
2525
2626newtype FunctionName = FunctionName { unFunctionName :: [Text] }
@@ -135,15 +135,6 @@ makeLenses ''Req
135135defReq :: Req ftype
136136defReq = Req defUrl "GET" [] Nothing Nothing (FunctionName [])
137137
138−-- | To be used exclusively as a "negative" return type/constraint
139−-- by @'Elem`@ type family.
140−class NotFound
141−
142−type family Elem (a :: *) (ls::[*]) :: Constraint where
143− Elem a '[] = NotFound
144− Elem a (a ': list) = ()
145− Elem a (b ': list) = Elem a list
146−
147138-- | 'HasForeignType' maps Haskell types with types in the target
148139-- language of your backend. For example, let's say you're
149140-- implementing a backend to some language __X__, and you want
servant/CHANGELOG.md+6
@@ -8,6 +8,10 @@
88 use its `ToHttpApiData` instance or `linkURI`.
99 ([#527](https://github.com/haskell-servant/servant/issues/527))
1010
11+* Add `Servant.API.TypeLevel` module with type families to work with API types.
12+ ([#345](https://github.com/haskell-servant/servant/pull/345))
13+ ([#305](https://github.com/haskell-servant/servant/issues/305))
14+
11150.9.1
1216------
1317
@@ -24,6 +28,8 @@
2428----
2529
2630* Add `CaptureAll` combinator. Captures all of the remaining segments in a URL.
31+* Add `Servant.API.TypeLevel` module, with frequently used type-level
32+functionaliy.
2733
28340.8
2935---
servant/servant.cabal+1
@@ -47,6 +47,7 @@ library
4747 Servant.API.ReqBody
4848 Servant.API.ResponseHeaders
4949 Servant.API.Sub
50+ Servant.API.TypeLevel
5051 Servant.API.Vault
5152 Servant.API.Verbs
5253 Servant.API.WithNamedContext
servant/src/Servant/API/TypeLevel.hsnew file+264
@@ -0,0 +1,264 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE KindSignatures #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE PolyKinds #-}
7+{-# LANGUAGE TypeFamilies #-}
8+{-# LANGUAGE TypeOperators #-}
9+{-# LANGUAGE UndecidableInstances #-}
10+
11+{-|
12+This module collects utilities for manipulating @servant@ API types. The
13+functionality in this module is for advanced usage.
14+
15+The code samples in this module use the following type synonym:
16+
17+> type SampleAPI = "hello" :> Get '[JSON] Int
18+> :<|> "bye" :> Capture "name" String :> Post '[JSON, PlainText] Bool
19+
20+-}
21+module Servant.API.TypeLevel (
22+ -- $setup
23+ -- * API predicates
24+ Endpoints,
25+ -- ** Lax inclusion
26+ IsElem',
27+ IsElem,
28+ IsSubAPI,
29+ AllIsElem,
30+ -- ** Strict inclusion
31+ IsIn,
32+ IsStrictSubAPI,
33+ AllIsIn,
34+ -- * Helpers
35+ -- ** Lists
36+ MapSub,
37+ AppendList,
38+ IsSubList,
39+ Elem,
40+ ElemGo,
41+ -- ** Logic
42+ Or,
43+ And,
44+ -- * Custom type errors
45+ -- | Before @base-4.9.0.0@ we use non-exported 'ElemNotFoundIn' class,
46+ -- which cannot be instantiated.
47+ ) where
48+
49+
50+import GHC.Exts (Constraint)
51+import Servant.API.Alternative (type (:<|>))
52+import Servant.API.Capture (Capture, CaptureAll)
53+import Servant.API.Header (Header)
54+import Servant.API.QueryParam (QueryFlag, QueryParam, QueryParams)
55+import Servant.API.ReqBody (ReqBody)
56+import Servant.API.Sub (type (:>))
57+import Servant.API.Verbs (Verb)
58+#if MIN_VERSION_base(4,9,0)
59+import GHC.TypeLits (TypeError, ErrorMessage(..))
60+#endif
61+
62+
63+
64+-- * API predicates
65+
66+-- | Flatten API into a list of endpoints.
67+--
68+-- >>> Refl :: Endpoints SampleAPI :~: '["hello" :> Verb 'GET 200 '[JSON] Int, "bye" :> (Capture "name" String :> Verb 'POST 200 '[JSON, PlainText] Bool)]
69+-- Refl
70+type family Endpoints api where
71+ Endpoints (a :<|> b) = AppendList (Endpoints a) (Endpoints b)
72+ Endpoints (e :> a) = MapSub e (Endpoints a)
73+ Endpoints a = '[a]
74+
75+-- ** Lax inclusion
76+
77+-- | You may use this type family to tell the type checker that your custom
78+-- type may be skipped as part of a link. This is useful for things like
79+-- @'QueryParam'@ that are optional in a URI and do not affect them if they are
80+-- omitted.
81+--
82+-- >>> data CustomThing
83+-- >>> type instance IsElem' e (CustomThing :> s) = IsElem e s
84+--
85+-- Note that @'IsElem'@ is called, which will mutually recurse back to @'IsElem''@
86+-- if it exhausts all other options again.
87+--
88+-- Once you have written a @HasLink@ instance for @CustomThing@ you are ready to go.
89+type family IsElem' a s :: Constraint
90+
91+-- | Closed type family, check if @endpoint@ is within @api@.
92+-- Uses @'IsElem''@ if it exhausts all other options.
93+--
94+-- >>> ok (Proxy :: Proxy (IsElem ("hello" :> Get '[JSON] Int) SampleAPI))
95+-- OK
96+--
97+-- >>> ok (Proxy :: Proxy (IsElem ("bye" :> Get '[JSON] Int) SampleAPI))
98+-- ...
99+-- ... Could not deduce...
100+-- ...
101+--
102+-- An endpoint is considered within an api even if it is missing combinators
103+-- that don't affect the URL:
104+--
105+-- >>> ok (Proxy :: Proxy (IsElem (Get '[JSON] Int) (Header "h" Bool :> Get '[JSON] Int)))
106+-- OK
107+--
108+-- >>> ok (Proxy :: Proxy (IsElem (Get '[JSON] Int) (ReqBody '[JSON] Bool :> Get '[JSON] Int)))
109+-- OK
110+--
111+-- *N.B.:* @IsElem a b@ can be seen as capturing the notion of whether the URL
112+-- represented by @a@ would match the URL represented by @b@, *not* whether a
113+-- request represented by @a@ matches the endpoints serving @b@ (for the
114+-- latter, use 'IsIn').
115+type family IsElem endpoint api :: Constraint where
116+ IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
117+ IsElem (e :> sa) (e :> sb) = IsElem sa sb
118+ IsElem sa (Header sym x :> sb) = IsElem sa sb
119+ IsElem sa (ReqBody y x :> sb) = IsElem sa sb
120+ IsElem (CaptureAll z y :> sa) (CaptureAll x y :> sb)
121+ = IsElem sa sb
122+ IsElem (Capture z y :> sa) (Capture x y :> sb)
123+ = IsElem sa sb
124+ IsElem sa (QueryParam x y :> sb) = IsElem sa sb
125+ IsElem sa (QueryParams x y :> sb) = IsElem sa sb
126+ IsElem sa (QueryFlag x :> sb) = IsElem sa sb
127+ IsElem (Verb m s ct typ) (Verb m s ct' typ)
128+ = IsSubList ct ct'
129+ IsElem e e = ()
130+ IsElem e a = IsElem' e a
131+
132+-- | Check whether @sub@ is a sub-API of @api@.
133+--
134+-- >>> ok (Proxy :: Proxy (IsSubAPI SampleAPI (SampleAPI :<|> Get '[JSON] Int)))
135+-- OK
136+--
137+-- >>> ok (Proxy :: Proxy (IsSubAPI (SampleAPI :<|> Get '[JSON] Int) SampleAPI))
138+-- ...
139+-- ... Could not deduce...
140+-- ...
141+--
142+-- This uses @IsElem@ for checking; thus the note there applies here.
143+type family IsSubAPI sub api :: Constraint where
144+ IsSubAPI sub api = AllIsElem (Endpoints sub) api
145+
146+-- | Check that every element of @xs@ is an endpoint of @api@ (using @'IsElem'@).
147+type family AllIsElem xs api :: Constraint where
148+ AllIsElem '[] api = ()
149+ AllIsElem (x ': xs) api = (IsElem x api, AllIsElem xs api)
150+
151+-- ** Strict inclusion
152+
153+-- | Closed type family, check if @endpoint@ is exactly within @api@.
154+--
155+-- >>> ok (Proxy :: Proxy (IsIn ("hello" :> Get '[JSON] Int) SampleAPI))
156+-- OK
157+--
158+-- Unlike 'IsElem', this requires an *exact* match.
159+--
160+-- >>> ok (Proxy :: Proxy (IsIn (Get '[JSON] Int) (Header "h" Bool :> Get '[JSON] Int)))
161+-- ...
162+-- ... Could not deduce...
163+-- ...
164+type family IsIn (endpoint :: *) (api :: *) :: Constraint where
165+ IsIn e (sa :<|> sb) = Or (IsIn e sa) (IsIn e sb)
166+ IsIn (e :> sa) (e :> sb) = IsIn sa sb
167+ IsIn e e = ()
168+
169+-- | Check whether @sub@ is a sub API of @api@.
170+--
171+-- Like 'IsSubAPI', but uses 'IsIn' rather than 'IsElem'.
172+type family IsStrictSubAPI sub api :: Constraint where
173+ IsStrictSubAPI sub api = AllIsIn (Endpoints sub) api
174+
175+-- | Check that every element of @xs@ is an endpoint of @api@ (using @'IsIn'@).
176+--
177+-- ok (Proxy :: Proxy (AllIsIn (Endpoints SampleAPI) SampleAPI))
178+-- OK
179+type family AllIsIn xs api :: Constraint where
180+ AllIsIn '[] api = ()
181+ AllIsIn (x ': xs) api = (IsIn x api, AllIsIn xs api)
182+
183+-- * Helpers
184+
185+-- ** Lists
186+
187+-- | Apply @(e :>)@ to every API in @xs@.
188+type family MapSub e xs where
189+ MapSub e '[] = '[]
190+ MapSub e (x ': xs) = (e :> x) ': MapSub e xs
191+
192+-- | Append two type-level lists.
193+type family AppendList xs ys where
194+ AppendList '[] ys = ys
195+ AppendList (x ': xs) ys = x ': AppendList xs ys
196+
197+type family IsSubList a b :: Constraint where
198+ IsSubList '[] b = ()
199+ IsSubList (x ': xs) y = Elem x y `And` IsSubList xs y
200+
201+-- | Check that a value is an element of a list:
202+--
203+-- >>> ok (Proxy :: Proxy (Elem Bool '[Int, Bool]))
204+-- OK
205+--
206+-- >>> ok (Proxy :: Proxy (Elem String '[Int, Bool]))
207+-- ...
208+-- ... [Char]...'[Int, Bool...
209+-- ...
210+type Elem e es = ElemGo e es es
211+
212+-- 'orig' is used to store original list for better error messages
213+type family ElemGo e es orig :: Constraint where
214+ ElemGo x (x ': xs) orig = ()
215+ ElemGo y (x ': xs) orig = ElemGo y xs orig
216+#if MIN_VERSION_base(4,9,0)
217+ -- Note [Custom Errors]
218+ ElemGo x '[] orig = TypeError ('ShowType x
219+ ':<>: 'Text " expected in list "
220+ ':<>: 'ShowType orig)
221+#else
222+ ElemGo x '[] orig = ElemNotFoundIn x orig
223+#endif
224+
225+-- ** Logic
226+
227+-- | If either a or b produce an empty constraint, produce an empty constraint.
228+type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
229+ -- This works because of:
230+ -- https://ghc.haskell.org/trac/ghc/wiki/NewAxioms/CoincidentOverlap
231+ Or () b = ()
232+ Or a () = ()
233+
234+-- | If both a or b produce an empty constraint, produce an empty constraint.
235+type family And (a :: Constraint) (b :: Constraint) :: Constraint where
236+ And () () = ()
237+
238+-- * Custom type errors
239+
240+#if !MIN_VERSION_base(4,9,0)
241+class ElemNotFoundIn val list
242+#endif
243+
244+{- Note [Custom Errors]
245+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
246+We might try to factor these our more cleanly, but the type synonyms and type
247+families are not evaluated (see https://ghc.haskell.org/trac/ghc/ticket/12048).
248+-}
249+
250+
251+-- $setup
252+--
253+-- The doctests in this module are run with following preamble:
254+--
255+-- >>> :set -XPolyKinds
256+-- >>> :set -XGADTs
257+-- >>> import Data.Proxy
258+-- >>> import Data.Type.Equality
259+-- >>> import Servant.API
260+-- >>> data OK ctx where OK :: ctx => OK ctx
261+-- >>> instance Show (OK ctx) where show _ = "OK"
262+-- >>> let ok :: ctx => Proxy ctx -> OK ctx; ok _ = OK
263+-- >>> type SampleAPI = "hello" :> Get '[JSON] Int :<|> "bye" :> Capture "name" String :> Post '[JSON, PlainText] Bool
264+-- >>> let sampleAPI = Proxy :: Proxy SampleAPI
servant/src/Servant/Utils/Links.hs+3−59
@@ -6,7 +6,6 @@
66{-# LANGUAGE ScopedTypeVariables #-}
77{-# LANGUAGE TypeFamilies #-}
88{-# LANGUAGE TypeOperators #-}
9−{-# LANGUAGE UndecidableInstances #-}
109{-# OPTIONS_HADDOCK not-home #-}
1110
1211-- | Type safe generation of internal links.
@@ -79,6 +78,8 @@
7978-- bad_link under api after trying the open (but empty) type family
8079-- `IsElem'` as a last resort.
8180module Servant.Utils.Links (
81+ module Servant.API.TypeLevel,
82+
8283 -- * Building and using safe links
8384 --
8485 -- | Note that 'URI' is Network.URI.URI from the network-uri package.
@@ -88,10 +89,6 @@ module Servant.Utils.Links (
8889 , HasLink(..)
8990 , linkURI
9091 , Link
91− , IsElem'
92− -- * Illustrative exports
93− , IsElem
94− , Or
9592) where
9693
9794import Data.List
@@ -99,7 +96,6 @@ import Data.Monoid.Compat ( (<>) )
9996import Data.Proxy ( Proxy(..) )
10097import qualified Data.Text as Text
10198import qualified Data.Text.Encoding as TE
102−import GHC.Exts (Constraint)
10399import GHC.TypeLits ( KnownSymbol, symbolVal )
104100import Network.URI ( URI(..), escapeURIString, isUnreserved )
105101import Prelude ()
@@ -115,7 +111,7 @@ import Servant.API.RemoteHost ( RemoteHost )
115111import Servant.API.Verbs ( Verb )
116112import Servant.API.Sub ( type (:>) )
117113import Servant.API.Raw ( Raw )
118−import Servant.API.Alternative ( type (:<|>) )
114+import Servant.API.TypeLevel
119115
120116-- | A safe link datatype.
121117-- The only way of constructing a 'Link' is using 'safeLink', which means any
@@ -131,58 +127,6 @@ instance ToHttpApiData Link where
131127 let uri = linkURI l
132128 in Text.pack $ uriPath uri ++ uriQuery uri
133129
134−-- | If either a or b produce an empty constraint, produce an empty constraint.
135−type family Or (a :: Constraint) (b :: Constraint) :: Constraint where
136− -- This works because of:
137− -- https://ghc.haskell.org/trac/ghc/wiki/NewAxioms/CoincidentOverlap
138− Or () b = ()
139− Or a () = ()
140−
141−-- | If both a or b produce an empty constraint, produce an empty constraint.
142−type family And (a :: Constraint) (b :: Constraint) :: Constraint where
143− And () () = ()
144−
145−-- | You may use this type family to tell the type checker that your custom
146−-- type may be skipped as part of a link. This is useful for things like
147−-- 'QueryParam' that are optional in a URI and do not affect them if they are
148−-- omitted.
149−--
150−-- >>> data CustomThing
151−-- >>> type instance IsElem' e (CustomThing :> s) = IsElem e s
152−--
153−-- Note that 'IsElem' is called, which will mutually recurse back to `IsElem'`
154−-- if it exhausts all other options again.
155−--
156−-- Once you have written a HasLink instance for CustomThing you are ready to
157−-- go.
158−type family IsElem' a s :: Constraint
159−
160−-- | Closed type family, check if endpoint is within api
161−type family IsElem endpoint api :: Constraint where
162− IsElem e (sa :<|> sb) = Or (IsElem e sa) (IsElem e sb)
163− IsElem (e :> sa) (e :> sb) = IsElem sa sb
164− IsElem sa (Header sym x :> sb) = IsElem sa sb
165− IsElem sa (ReqBody y x :> sb) = IsElem sa sb
166− IsElem (Capture z y :> sa) (Capture x y :> sb)
167− = IsElem sa sb
168− IsElem (CaptureAll z y :> sa) (CaptureAll x y :> sb)
169− = IsElem sa sb
170− IsElem sa (QueryParam x y :> sb) = IsElem sa sb
171− IsElem sa (QueryParams x y :> sb) = IsElem sa sb
172− IsElem sa (QueryFlag x :> sb) = IsElem sa sb
173− IsElem (Verb m s ct typ) (Verb m s ct' typ)
174− = IsSubList ct ct'
175− IsElem e e = ()
176− IsElem e a = IsElem' e a
177−
178−type family IsSubList a b :: Constraint where
179− IsSubList '[] b = ()
180− IsSubList (x ': xs) y = Elem x y `And` IsSubList xs y
181−
182−type family Elem e es :: Constraint where
183− Elem x (x ': xs) = ()
184− Elem y (x ': xs) = Elem y xs
185−
186130-- Phantom types for Param
187131data Query
188132