]> Git — Sourcephile - haskell/symantic-http.git/blob - symantic-http/Symantic/HTTP/API.hs
Add raw combinator
[haskell/symantic-http.git] / symantic-http / Symantic / HTTP / API.hs
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
8
9 import Control.Monad (Monad(..))
10 import Data.Bool
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)
19 import Prelude (and)
20 import System.IO (IO)
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
25
26 -- * Class 'Cat'
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).
30 --
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.
34 class Cat repr where
35 (<.>) :: repr a b -> repr b c -> repr a c; infixr 4 <.>
36 -- Trans defaults
37 default (<.>) ::
38 Trans repr =>
39 Cat (UnTrans repr) =>
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 .>
43
44 -- * Class 'Alt'
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.
48 class Alt repr where
49 {-
50 type AltMerge repr :: * -> * -> *
51 (<!>) :: repr a b -> repr c d -> repr (a:!:c) (AltMerge repr b d); infixl 3 <!>
52 -}
53 (<!>) :: repr a k -> repr b k -> repr (a:!:b) k; infixr 3 <!>
54 -- Trans defaults
55 default (<!>) ::
56 Trans repr =>
57 Alt (UnTrans repr) =>
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
62
63 -- ** Type (':!:')
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
68 infixr 3 :!:
69
70 -- * Class 'Trans'
71 -- | A 'Trans'formation from one representation @('UnTrans' repr)@ to another one @(repr)@.
72 --
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.
76 --
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)@.
81 --
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
95
96 -- * Class 'Pro'
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.
101 --
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'.
105 class Pro repr where
106 dimap :: (a -> b) -> (b -> a) -> repr (a -> k) k -> repr (b -> k) k
107 -- Trans defaults
108 default dimap ::
109 Trans repr =>
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
113
114 -- * Class 'HTTP_Raw'
115 class HTTP_Raw repr where
116 type RawConstraint repr :: Constraint
117 type RawArgs repr :: *
118 type Raw repr :: *
119 raw ::
120 RawConstraint repr =>
121 repr (RawArgs repr) (Raw repr)
122 -- Trans defaults
123 type RawConstraint repr = RawConstraint (UnTrans repr)
124 type RawArgs repr = RawArgs (UnTrans repr)
125 type Raw repr = Raw (UnTrans repr)
126 default raw ::
127 Trans 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)
133 raw = noTrans raw
134
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
141 -- Trans defaults
142 type PathConstraint repr a = PathConstraint (UnTrans repr) a
143 default segment ::
144 Trans repr =>
145 HTTP_Path (UnTrans repr) =>
146 PathSegment -> repr k k
147 default capture' ::
148 Trans repr =>
149 HTTP_Path (UnTrans repr) =>
150 PathConstraint (UnTrans repr) a =>
151 Name -> repr (a -> k) k
152 default captureAll ::
153 Trans repr =>
154 HTTP_Path (UnTrans repr) =>
155 repr ([PathSegment] -> k) k
156 segment = noTrans . segment
157 capture' = noTrans . capture'
158 captureAll = noTrans captureAll
159
160 -- | Convenient wrapper of 'segment'.
161 (</>) :: Cat repr => HTTP_Path repr => PathSegment -> repr a b -> repr a b
162 (</>) n = (segment n <.>); infixr 4 </>
163
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)@.
166 capture ::
167 forall a k repr.
168 HTTP_Path repr =>
169 PathConstraint repr a =>
170 Name -> repr (a -> k) k
171 capture = capture'
172 {-# INLINE capture #-}
173
174 type PathSegment = Text
175 type Path = [PathSegment]
176 type Name = String
177
178 -- * Class 'HTTP_Header'
179 class HTTP_Header repr where
180 header :: HTTP.HeaderName -> repr (HeaderValue -> k) k
181 -- Trans defaults
182 default header ::
183 Trans repr =>
184 HTTP_Header (UnTrans repr) =>
185 HTTP.HeaderName -> repr (HeaderValue -> k) k
186 header = noTrans . header
187
188 type HeaderValue = BS.ByteString
189
190 -- * Class 'HTTP_Body'
191 class HTTP_Body repr where
192 type BodyArg repr a (ts::[*]) :: *
193 type BodyConstraint repr a (ts::[*]) :: Constraint
194 body' ::
195 BodyConstraint repr a ts =>
196 repr (BodyArg repr a ts -> k) k
197 -- Trans defaults
198 type BodyArg repr a ts = BodyArg (UnTrans repr) a ts
199 type BodyConstraint repr a ts = BodyConstraint (UnTrans repr) a ts
200 default body' ::
201 forall a (ts::[*]) k.
202 Trans repr =>
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)
208
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)@.
211 body ::
212 forall a ts k repr.
213 HTTP_Body repr =>
214 BodyConstraint repr a ts =>
215 repr (BodyArg repr a ts -> k) k
216 body = body' @repr @a @ts
217 {-# INLINE body #-}
218
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
223 bodyStream' ::
224 BodyStreamConstraint repr as ts framing =>
225 repr (BodyStreamArg repr as ts framing -> k) k
226 -- Trans defaults
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.
231 Trans repr =>
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)
237
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)@.
241 bodyStream ::
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 #-}
248
249 -- * Class 'HTTP_Query'
250 class HTTP_Query repr where
251 type QueryConstraint repr a :: Constraint
252 queryParams' ::
253 QueryConstraint repr a =>
254 QueryName -> repr ([a] -> k) k
255 queryFlag ::
256 QueryConstraint repr Bool =>
257 QueryName -> repr (Bool -> k) k
258 default queryFlag ::
259 Pro repr =>
260 QueryConstraint repr Bool =>
261 QueryName -> repr (Bool -> k) k
262 queryFlag n = dimap and return (queryParams' n)
263 -- Trans defaults
264 type QueryConstraint repr a = QueryConstraint (UnTrans repr) a
265 default queryParams' ::
266 Trans repr =>
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
273
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)@.
276 queryParams ::
277 forall a k repr.
278 HTTP_Query repr =>
279 QueryConstraint repr a =>
280 QueryName -> repr ([a] -> k) k
281 queryParams = queryParams'
282 {-# INLINE queryParams #-}
283
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 :: *
289 basicAuth' ::
290 BasicAuthConstraint repr a =>
291 BasicAuthRealm -> repr (BasicAuthArgs repr a k) k
292 -- Trans defaults
293 type BasicAuthConstraint repr a = BasicAuthConstraint (UnTrans repr) a
294 type BasicAuthArgs repr a k = BasicAuthArgs (UnTrans repr) a k
295 default basicAuth' ::
296 forall a k.
297 Trans repr =>
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
303
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)@.
306 basicAuth ::
307 forall a k repr.
308 HTTP_BasicAuth repr =>
309 BasicAuthConstraint repr a =>
310 BasicAuthRealm ->
311 repr (BasicAuthArgs repr a k) k
312 basicAuth = basicAuth' @repr @a @k
313 {-# INLINE basicAuth #-}
314
315 -- ** Type 'BasicAuth'
316 data BasicAuth usr
317 = BasicAuth_Authorized usr
318 | BasicAuth_BadPassword
319 | BasicAuth_NoSuchUser
320 | BasicAuth_Unauthorized
321 deriving (Eq, Show, Functor)
322
323 type BasicAuthRealm = Text
324 type BasicAuthUser = Text
325 type BasicAuthPass = Text
326
327 -- * Class 'HTTP_Version'
328 class HTTP_Version repr where
329 version :: HTTP.HttpVersion -> repr k k
330
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
335
336 -- ** Type 'StatusIs'
337 data StatusIs
338 = StatusIsInformational
339 | StatusIsSuccessful
340 | StatusIsRedirection
341 | StatusIsClientError
342 | StatusIsServerError
343 | StatusIs HTTP.Status
344 deriving (Eq, Ord, Show)
345 statusIs :: StatusIs -> (HTTP.Status -> Bool)
346 statusIs = \case
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
353 -}
354
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 :: *
360 response ::
361 ResponseConstraint repr a ts =>
362 HTTP.Method ->
363 repr (ResponseArgs repr a ts)
364 (Response repr)
365 -- Trans defaults
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)
369 default response ::
370 forall a ts.
371 Trans 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) =>
376 HTTP.Method ->
377 repr (ResponseArgs repr a ts)
378 (Response repr)
379 response = noTrans . response @_ @a @ts
380
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 ::
386 forall a ts repr.
387 HTTP_Response repr =>
388 ResponseConstraint repr a ts =>
389 repr (ResponseArgs repr a ts)
390 (Response repr)
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
400 {-# INLINE get #-}
401 {-# INLINE head #-}
402 {-# INLINE put #-}
403 {-# INLINE post #-}
404 {-# INLINE delete #-}
405 {-# INLINE trace #-}
406 {-# INLINE connect #-}
407 {-# INLINE options #-}
408 {-# INLINE patch #-}
409
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 :: *
415 responseStream ::
416 ResponseStreamConstraint repr as ts framing =>
417 HTTP.Method ->
418 repr (ResponseStreamArgs repr as ts framing)
419 (ResponseStream repr)
420 -- Trans defaults
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.
426 Trans repr =>
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) =>
431 HTTP.Method ->
432 repr (ResponseStreamArgs repr as ts framing)
433 (ResponseStream repr)
434 responseStream = noTrans . responseStream @_ @as @ts @framing
435
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 #-}
460
461 -- * Framing
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 :: *
468
469 -- ** Class 'FramingEncode'
470 class FramingEncode framing p where
471 framingEncode ::
472 Proxy framing ->
473 {-mimeEncode-}(FramingYield p -> BSL.ByteString) ->
474 p -> IO (Either (FramingReturn p) (BSL.ByteString, p))
475
476 -- ** Class 'FramingDecode'
477 class FramingDecode framing p where
478 framingDecode ::
479 FramingMonad p ~ m =>
480 Monad m =>
481 Proxy framing ->
482 {-mimeDecode-}(BSL.ByteString -> Either String (FramingYield p)) ->
483 m BS.ByteString -> p
484
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.
489 data NoFraming
490
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).
496 data NewlineFraming
497
498 -- ** Type 'NetstringFraming'
499 -- | The netstring framing strategy as defined by djb:
500 -- <http://cr.yp.to/proto/netstrings.txt>
501 --
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.
508 --
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