1 {-# LANGUAGE AllowAmbiguousTypes #-}
2 {-# LANGUAGE DefaultSignatures #-}
3 {-# LANGUAGE DeriveFunctor #-}
4 {-# LANGUAGE StrictData #-}
5 {-# LANGUAGE UndecidableInstances #-} -- for type instance defaults
6 -- | Combinators to build a Web API.
7 module Symantic.HTTP.API where
9 import Control.Monad (Monad(..))
11 import Data.Either (Either(..))
12 import Data.Eq (Eq(..))
13 import Data.Function ((.))
14 import Data.Functor (Functor)
15 import Data.Kind (Constraint)
16 import Data.Proxy (Proxy)
17 import Data.String (String)
18 import Data.Text (Text)
21 import Text.Show (Show(..))
22 import qualified Data.ByteString as BS
23 import qualified Data.ByteString.Lazy as BSL
24 import qualified Network.HTTP.Types as HTTP
27 -- | A soft and cute animal asking strokes and croquettes.
28 -- Or rather here a composition of two combinators
29 -- (as in a category without an identity morphism).
31 -- Note that the order of combinators generally matters (the left one is applied first),
32 -- with the notable exception of the server instance
33 -- where some HTTP error codes must be prioritized.
35 (<.>) :: repr a b -> repr b c -> repr a c; infixr 4 <.>
40 repr a b -> repr b c -> repr a c
41 x <.> y = noTrans (unTrans x <.> unTrans y)
42 -- (.>) :: repr x y -> repr a c -> repr a c; infixl 4 .>
45 -- | There are two choices, either the right one or the left one.
46 -- The (':!:') data type will be used in the instances
47 -- to get multiple client callers or to supply multiple server handlers.
50 type AltMerge repr :: * -> * -> *
51 (<!>) :: repr a b -> repr c d -> repr (a:!:c) (AltMerge repr b d); infixl 3 <!>
53 (<!>) :: repr a k -> repr b k -> repr (a:!:b) k; infixr 3 <!>
58 repr a k -> repr b k -> repr (a:!:b) k
59 x <!> y = noTrans (unTrans x <!> unTrans y)
60 -- try :: repr k k -> repr k k
61 -- option :: k -> repr k k -> repr k k
64 -- | Like @(,)@ but @infixr@.
65 -- Used to get alternative commands from a 'Client'
66 -- or to supply alternative handlers to a 'Server'.
67 data (:!:) a b = a:!:b
71 -- | A 'Trans'formation from one representation @('UnTrans' repr)@ to another one @(repr)@.
73 -- * 'noTrans' lifts to the identity 'Trans'formation
74 -- (the one which does nothing wrt. the 'UnTrans'formed @(repr)@esentation).
75 -- * 'unTrans' unlifts a 'Trans'formed value to its underlying @(repr)@esentation.
77 -- At its @class@ definition,
78 -- a combinator should be defined with a default value using 'noTrans'.
79 -- And at its @instance@ definition,
80 -- a combinator can be overwritten to apply a specific 'Trans'formation for @(repr)@.
82 -- For an example, see the @('Trans' ('Router' repr))@ instance
83 -- in <https://hackage.haskell.org/package/symantic-http-server symantic-http-server>.
84 class Trans repr where
85 -- | The @(repr)@esentation that @(repr)@ 'Trans'forms.
86 type UnTrans repr :: * -> * -> *
87 -- | Lift the underlying @(repr)@esentation to @(repr)@.
88 -- Useful to define a combinator that does nothing in a 'Trans'formation.
89 noTrans :: UnTrans repr a b -> repr a b
90 -- | Unlift a @(repr)@esentation. Useful when a 'Trans'formation
91 -- combinator needs to access the 'UnTrans'formed @(repr)@esentation,
92 -- or at the end to get the underlying 'UnTrans'formed @(repr)@esentation
93 -- from the inferred @(repr)@ value (eg. in 'server').
94 unTrans :: repr a b -> UnTrans repr a b
97 -- | Mainly useful to write a combinator which is a specialization of another
98 -- (eg. 'queryFlag' wrt. 'queryParams'),
99 -- by calling it directly in the class declaration
100 -- instead of rewriting its logic in the instance declaration.
102 -- Because type @(a)@ is asked by a 'Client' but given to a 'Server',
103 -- both @(a->b)@ and @(b->a)@ are used. This is reminiscent of a 'Profunctor'.
104 -- Hence the names 'Pro' and 'dimap'.
106 dimap :: (a -> b) -> (b -> a) -> repr (a -> k) k -> repr (b -> k) k
110 Pro (UnTrans repr) =>
111 (a -> b) -> (b -> a) -> repr (a -> k) k -> repr (b -> k) k
112 dimap a2b b2a = noTrans . dimap a2b b2a . unTrans
114 -- * Class 'HTTP_Raw'
115 class HTTP_Raw repr where
116 type RawConstraint repr :: Constraint
117 type RawArgs repr :: *
120 RawConstraint repr =>
121 repr (RawArgs repr) (Raw repr)
123 type RawConstraint repr = RawConstraint (UnTrans repr)
124 type RawArgs repr = RawArgs (UnTrans repr)
125 type Raw repr = Raw (UnTrans repr)
128 HTTP_Raw (UnTrans repr) =>
129 RawConstraint (UnTrans repr) =>
130 RawArgs (UnTrans repr) ~ RawArgs repr =>
131 Raw (UnTrans repr) ~ Raw repr =>
132 repr (RawArgs repr) (Raw repr)
135 -- * Class 'HTTP_Path'
136 class HTTP_Path repr where
137 type PathConstraint repr a :: Constraint
138 segment :: PathSegment -> repr k k
139 capture' :: PathConstraint repr a => Name -> repr (a -> k) k
140 captureAll :: repr ([PathSegment] -> k) k
142 type PathConstraint repr a = PathConstraint (UnTrans repr) a
145 HTTP_Path (UnTrans repr) =>
146 PathSegment -> repr k k
149 HTTP_Path (UnTrans repr) =>
150 PathConstraint (UnTrans repr) a =>
151 Name -> repr (a -> k) k
152 default captureAll ::
154 HTTP_Path (UnTrans repr) =>
155 repr ([PathSegment] -> k) k
156 segment = noTrans . segment
157 capture' = noTrans . capture'
158 captureAll = noTrans captureAll
160 -- | Convenient wrapper of 'segment'.
161 (</>) :: Cat repr => HTTP_Path repr => PathSegment -> repr a b -> repr a b
162 (</>) n = (segment n <.>); infixr 4 </>
164 -- | Like 'capture'' but with the type variable @(a)@ first instead or @(repr)@
165 -- so it can be passed using 'TypeApplications' without adding a |\@_| for @(repr)@.
169 PathConstraint repr a =>
170 Name -> repr (a -> k) k
172 {-# INLINE capture #-}
174 type PathSegment = Text
175 type Path = [PathSegment]
178 -- * Class 'HTTP_Header'
179 class HTTP_Header repr where
180 header :: HTTP.HeaderName -> repr (HeaderValue -> k) k
184 HTTP_Header (UnTrans repr) =>
185 HTTP.HeaderName -> repr (HeaderValue -> k) k
186 header = noTrans . header
188 type HeaderValue = BS.ByteString
190 -- * Class 'HTTP_Body'
191 class HTTP_Body repr where
192 type BodyArg repr a (ts::[*]) :: *
193 type BodyConstraint repr a (ts::[*]) :: Constraint
195 BodyConstraint repr a ts =>
196 repr (BodyArg repr a ts -> k) k
198 type BodyArg repr a ts = BodyArg (UnTrans repr) a ts
199 type BodyConstraint repr a ts = BodyConstraint (UnTrans repr) a ts
201 forall a (ts::[*]) k.
203 HTTP_Body (UnTrans repr) =>
204 BodyConstraint (UnTrans repr) a ts =>
205 BodyArg repr a ts ~ BodyArg (UnTrans repr) a ts =>
206 repr (BodyArg repr a ts -> k) k
207 body' = noTrans (body' @_ @a @ts)
209 -- | Like 'body'' but with the type variables @(a)@ and @(ts)@ first instead or @(repr)@,
210 -- so it can be passed using 'TypeApplications' without adding a |\@_| for @(repr)@.
214 BodyConstraint repr a ts =>
215 repr (BodyArg repr a ts -> k) k
216 body = body' @repr @a @ts
219 -- * Class 'HTTP_BodyStream'
220 class HTTP_BodyStream repr where
221 type BodyStreamArg repr as (ts::[*]) framing :: *
222 type BodyStreamConstraint repr as (ts::[*]) framing :: Constraint
224 BodyStreamConstraint repr as ts framing =>
225 repr (BodyStreamArg repr as ts framing -> k) k
227 type BodyStreamArg repr as ts framing = BodyStreamArg (UnTrans repr) as ts framing
228 type BodyStreamConstraint repr as ts framing = BodyStreamConstraint (UnTrans repr) as ts framing
229 default bodyStream' ::
230 forall as ts framing k.
232 HTTP_BodyStream (UnTrans repr) =>
233 BodyStreamConstraint (UnTrans repr) as ts framing =>
234 BodyStreamArg repr as ts framing ~ BodyStreamArg (UnTrans repr) as ts framing =>
235 repr (BodyStreamArg repr as ts framing -> k) k
236 bodyStream' = noTrans (bodyStream' @_ @as @ts @framing)
238 -- | Like 'bodyStream'' but with the type variables @(as)@, @(ts)@ and @(framing)@
239 -- first instead or @(repr)@, so it can be passed using 'TypeApplications'
240 -- without adding a |\@_| for @(repr)@.
242 forall as ts framing k repr.
243 HTTP_BodyStream repr =>
244 BodyStreamConstraint repr as ts framing =>
245 repr (BodyStreamArg repr as ts framing -> k) k
246 bodyStream = bodyStream' @repr @as @ts @framing
247 {-# INLINE bodyStream #-}
249 -- * Class 'HTTP_Query'
250 class HTTP_Query repr where
251 type QueryConstraint repr a :: Constraint
253 QueryConstraint repr a =>
254 QueryName -> repr ([a] -> k) k
256 QueryConstraint repr Bool =>
257 QueryName -> repr (Bool -> k) k
260 QueryConstraint repr Bool =>
261 QueryName -> repr (Bool -> k) k
262 queryFlag n = dimap and return (queryParams' n)
264 type QueryConstraint repr a = QueryConstraint (UnTrans repr) a
265 default queryParams' ::
267 HTTP_Query (UnTrans repr) =>
268 QueryConstraint (UnTrans repr) a =>
269 QueryName -> repr ([a] -> k) k
270 queryParams' = noTrans . queryParams'
271 type QueryName = BS.ByteString
272 type QueryValue = BS.ByteString
274 -- | Like 'capture'' but with the type variable @(a)@ first instead or @(repr)@
275 -- so it can be passed using 'TypeApplications' without adding a |\@_| for @(repr)@.
279 QueryConstraint repr a =>
280 QueryName -> repr ([a] -> k) k
281 queryParams = queryParams'
282 {-# INLINE queryParams #-}
284 -- * Class 'HTTP_BasicAuth'
285 -- | <https://tools.ietf.org/html/rfc2617#section-2 Basic Access Authentication>
286 class HTTP_BasicAuth repr where
287 type BasicAuthConstraint repr a :: Constraint
288 type BasicAuthArgs repr a k :: *
290 BasicAuthConstraint repr a =>
291 BasicAuthRealm -> repr (BasicAuthArgs repr a k) k
293 type BasicAuthConstraint repr a = BasicAuthConstraint (UnTrans repr) a
294 type BasicAuthArgs repr a k = BasicAuthArgs (UnTrans repr) a k
295 default basicAuth' ::
298 HTTP_BasicAuth (UnTrans repr) =>
299 BasicAuthConstraint (UnTrans repr) a =>
300 BasicAuthArgs repr a k ~ BasicAuthArgs (UnTrans repr) a k =>
301 BasicAuthRealm -> repr (BasicAuthArgs repr a k) k
302 basicAuth' = noTrans . basicAuth' @_ @a
304 -- | Like 'basicAuth'' but with the type variable @(a)@ first instead or @(repr)@
305 -- so it can be passed using 'TypeApplications' without adding a |\@_| for @(repr)@.
308 HTTP_BasicAuth repr =>
309 BasicAuthConstraint repr a =>
311 repr (BasicAuthArgs repr a k) k
312 basicAuth = basicAuth' @repr @a @k
313 {-# INLINE basicAuth #-}
315 -- ** Type 'BasicAuth'
317 = BasicAuth_Authorized usr
318 | BasicAuth_BadPassword
319 | BasicAuth_NoSuchUser
320 | BasicAuth_Unauthorized
321 deriving (Eq, Show, Functor)
323 type BasicAuthRealm = Text
324 type BasicAuthUser = Text
325 type BasicAuthPass = Text
327 -- * Class 'HTTP_Version'
328 class HTTP_Version repr where
329 version :: HTTP.HttpVersion -> repr k k
331 {- TODO: see if this is useful somewhere.
332 -- * Class 'HTTP_Status'
333 class HTTP_Status repr where
334 status :: StatusIs -> repr (HTTP.Status -> k) k
336 -- ** Type 'StatusIs'
338 = StatusIsInformational
340 | StatusIsRedirection
341 | StatusIsClientError
342 | StatusIsServerError
343 | StatusIs HTTP.Status
344 deriving (Eq, Ord, Show)
345 statusIs :: StatusIs -> (HTTP.Status -> Bool)
347 StatusIsInformational -> HTTP.statusIsInformational
348 StatusIsSuccessful -> HTTP.statusIsSuccessful
349 StatusIsRedirection -> HTTP.statusIsRedirection
350 StatusIsClientError -> HTTP.statusIsClientError
351 StatusIsServerError -> HTTP.statusIsServerError
352 StatusIs x -> \y -> HTTP.statusCode x == HTTP.statusCode y
355 -- * Class 'HTTP_Response'
356 class HTTP_Response repr where
357 type ResponseConstraint repr a (ts::[*]) :: Constraint
358 type ResponseArgs repr a (ts::[*]) :: *
359 type Response repr :: *
361 ResponseConstraint repr a ts =>
363 repr (ResponseArgs repr a ts)
366 type ResponseConstraint repr a ts = ResponseConstraint (UnTrans repr) a ts
367 type ResponseArgs repr a ts = ResponseArgs (UnTrans repr) a ts
368 type Response repr = Response (UnTrans repr)
372 HTTP_Response (UnTrans repr) =>
373 ResponseConstraint (UnTrans repr) a ts =>
374 ResponseArgs repr a ts ~ ResponseArgs (UnTrans repr) a ts =>
375 Response repr ~ Response (UnTrans repr) =>
377 repr (ResponseArgs repr a ts)
379 response = noTrans . response @_ @a @ts
381 -- | Wrap 'response' by giving it the corresponding 'HTTP.Method',
382 -- and put the type variables @(a)@ then @(ts)@ first instead or @(repr)@
383 -- so they can be passed using 'TypeApplications'
384 -- without adding a |@_| for @(repr)@.
385 get,head,put,post,delete,trace,connect,options,patch ::
387 HTTP_Response repr =>
388 ResponseConstraint repr a ts =>
389 repr (ResponseArgs repr a ts)
391 get = response @repr @a @ts HTTP.methodGet
392 head = response @repr @a @ts HTTP.methodHead
393 put = response @repr @a @ts HTTP.methodPut
394 post = response @repr @a @ts HTTP.methodPost
395 delete = response @repr @a @ts HTTP.methodDelete
396 trace = response @repr @a @ts HTTP.methodTrace
397 connect = response @repr @a @ts HTTP.methodConnect
398 options = response @repr @a @ts HTTP.methodOptions
399 patch = response @repr @a @ts HTTP.methodPatch
404 {-# INLINE delete #-}
406 {-# INLINE connect #-}
407 {-# INLINE options #-}
410 -- * Class 'HTTP_ResponseStream'
411 class HTTP_ResponseStream repr where
412 type ResponseStreamConstraint repr as (ts::[*]) framing :: Constraint
413 type ResponseStreamArgs repr as (ts::[*]) framing :: *
414 type ResponseStream repr :: *
416 ResponseStreamConstraint repr as ts framing =>
418 repr (ResponseStreamArgs repr as ts framing)
419 (ResponseStream repr)
421 type ResponseStreamConstraint repr as ts framing = ResponseStreamConstraint (UnTrans repr) as ts framing
422 type ResponseStreamArgs repr as ts framing = ResponseStreamArgs (UnTrans repr) as ts framing
423 type ResponseStream repr = ResponseStream (UnTrans repr)
424 default responseStream ::
425 forall as ts framing.
427 HTTP_ResponseStream (UnTrans repr) =>
428 ResponseStreamConstraint (UnTrans repr) as ts framing =>
429 ResponseStreamArgs repr as ts framing ~ ResponseStreamArgs (UnTrans repr) as ts framing =>
430 ResponseStream repr ~ ResponseStream (UnTrans repr) =>
432 repr (ResponseStreamArgs repr as ts framing)
433 (ResponseStream repr)
434 responseStream = noTrans . responseStream @_ @as @ts @framing
436 getStream,headStream,putStream,postStream,deleteStream,traceStream,connectStream,optionsStream,patchStream ::
437 forall as ts framing repr.
438 HTTP_ResponseStream repr =>
439 ResponseStreamConstraint repr as ts framing =>
440 repr (ResponseStreamArgs repr as ts framing)
441 (ResponseStream repr)
442 getStream = responseStream @repr @as @ts @framing HTTP.methodGet
443 headStream = responseStream @repr @as @ts @framing HTTP.methodHead
444 putStream = responseStream @repr @as @ts @framing HTTP.methodPut
445 postStream = responseStream @repr @as @ts @framing HTTP.methodPost
446 deleteStream = responseStream @repr @as @ts @framing HTTP.methodDelete
447 traceStream = responseStream @repr @as @ts @framing HTTP.methodTrace
448 connectStream = responseStream @repr @as @ts @framing HTTP.methodConnect
449 optionsStream = responseStream @repr @as @ts @framing HTTP.methodOptions
450 patchStream = responseStream @repr @as @ts @framing HTTP.methodPatch
451 {-# INLINE getStream #-}
452 {-# INLINE headStream #-}
453 {-# INLINE putStream #-}
454 {-# INLINE postStream #-}
455 {-# INLINE deleteStream #-}
456 {-# INLINE traceStream #-}
457 {-# INLINE connectStream #-}
458 {-# INLINE optionsStream #-}
459 {-# INLINE patchStream #-}
462 -- ** Type family 'FramingMonad'
463 type family FramingMonad p :: * -> *
464 -- ** Type family 'FramingYield'
465 type family FramingYield p :: *
466 -- ** Type family 'FramingReturn'
467 type family FramingReturn p :: *
469 -- ** Class 'FramingEncode'
470 class FramingEncode framing p where
473 {-mimeEncode-}(FramingYield p -> BSL.ByteString) ->
474 p -> IO (Either (FramingReturn p) (BSL.ByteString, p))
476 -- ** Class 'FramingDecode'
477 class FramingDecode framing p where
479 FramingMonad p ~ m =>
482 {-mimeDecode-}(BSL.ByteString -> Either String (FramingYield p)) ->
485 -- ** Type 'NoFraming'
486 -- | A framing strategy that does not do any framing at all,
487 -- it just passes the input data.
488 -- Most of the time this will be used with binary data, such as files.
491 -- ** Type 'NewlineFraming'
492 -- | A simple framing strategy that has no header,
493 -- and inserts a newline character after each frame.
494 -- WARNING: this assumes that it is used with a Content-Type
495 -- that encodes without newlines (e.g. JSON).
498 -- ** Type 'NetstringFraming'
499 -- | The netstring framing strategy as defined by djb:
500 -- <http://cr.yp.to/proto/netstrings.txt>
502 -- Any string of 8-bit bytes may be encoded as @[len]":"[string]","@. Here
503 -- @[string]@ is the string and @[len]@ is a nonempty sequence of ASCII digits
504 -- giving the length of @[string]@ in decimal. The ASCII digits are @<30>@ for
505 -- 0, @<31>@ for 1, and so on up through @<39>@ for 9. Extra zeros at the front
506 -- of @[len]@ are prohibited: @[len]@ begins with @<30>@ exactly when
507 -- @[string]@ is empty.
509 -- For example, the string @"hello world!"@ is encoded as
510 -- @<31 32 3a 68 65 6c 6c 6f 20 77 6f 72 6c 64 21 2c>@,
511 -- i.e., @"12:hello world!,"@.
512 -- The empty string is encoded as @"0:,"@.
513 data NetstringFraming