|
1 | 1 | {-# LANGUAGE DataKinds #-}
|
2 | 2 | {-# LANGUAGE TypeFamilies #-}
|
| 3 | +{-# LANGUAGE InstanceSigs #-} |
3 | 4 | {-# LANGUAGE TypeOperators #-}
|
4 | 5 | {-# LANGUAGE RecordWildCards #-}
|
5 | 6 | {-# LANGUAGE FlexibleContexts #-}
|
@@ -27,22 +28,28 @@ module Servant.Multipart
|
27 | 28 | , defaultTmpBackendOptions
|
28 | 29 | , Input(..)
|
29 | 30 | , FileData(..)
|
| 31 | + -- * servant-docs |
| 32 | + , ToMultipartSample(..) |
30 | 33 | ) where
|
31 | 34 |
|
| 35 | +import Control.Lens ((<>~), (&), view) |
32 | 36 | import Control.Monad
|
33 | 37 | import Control.Monad.IO.Class
|
34 | 38 | import Control.Monad.Trans.Resource
|
35 | 39 | import Data.ByteString.Lazy (ByteString)
|
| 40 | +import Data.Foldable (foldMap) |
36 | 41 | import Data.Function
|
37 | 42 | import Data.List (find)
|
38 | 43 | import Data.Maybe
|
39 |
| -import Data.Text (Text) |
| 44 | +import Data.Monoid |
| 45 | +import Data.Text (Text, unpack) |
40 | 46 | import Data.Text.Encoding (decodeUtf8)
|
41 | 47 | import Data.Typeable
|
42 | 48 | import Network.HTTP.Media ((//))
|
43 | 49 | import Network.Wai
|
44 | 50 | import Network.Wai.Parse
|
45 | 51 | import Servant
|
| 52 | +import Servant.Docs |
46 | 53 | import Servant.Server.Internal
|
47 | 54 | import System.Directory
|
48 | 55 | import System.IO
|
@@ -418,3 +425,96 @@ instance {-# OVERLAPPING #-}
|
418 | 425 | instance HasLink sub => HasLink (MultipartForm a :> sub) where
|
419 | 426 | type MkLink (MultipartForm a :> sub) = MkLink sub
|
420 | 427 | toLink _ = toLink (Proxy :: Proxy sub)
|
| 428 | + |
| 429 | +-- | The 'ToMultipartSample' class allows you to create sample 'MultipartData' |
| 430 | +-- inputs for your type for use with "Servant.Docs". This is used by the |
| 431 | +-- 'HasDocs' instance for 'MultipartForm'. |
| 432 | +-- |
| 433 | +-- Given the example 'User' type and 'FromMultipart' instance above, here is a |
| 434 | +-- corresponding 'ToMultipartSample' instance: |
| 435 | +-- |
| 436 | +-- @ |
| 437 | +-- data User = User { username :: Text, pic :: FilePath } |
| 438 | +-- |
| 439 | +-- instance 'ToMultipartSample' 'Tmp' User where |
| 440 | +-- 'toMultipartSamples' proxy = |
| 441 | +-- [ ( \"sample 1\" |
| 442 | +-- , 'MultipartData' |
| 443 | +-- [ 'Input' \"username\" \"Elvis Presley\" ] |
| 444 | +-- [ 'FileData' |
| 445 | +-- \"pic\" |
| 446 | +-- \"playing_guitar.jpeg\" |
| 447 | +-- \"image/jpeg\" |
| 448 | +-- \"/tmp/servant-multipart000.buf\" |
| 449 | +-- ] |
| 450 | +-- ) |
| 451 | +-- ] |
| 452 | +-- @ |
| 453 | +class ToMultipartSample tag a where |
| 454 | + toMultipartSamples :: Proxy a -> [(Text, MultipartData tag)] |
| 455 | + |
| 456 | +-- | Format an 'Input' into a markdown list item. |
| 457 | +multipartInputToItem :: Input -> Text |
| 458 | +multipartInputToItem (Input name val) = |
| 459 | + " - *" <> name <> "*: " <> "`" <> val <> "`" |
| 460 | + |
| 461 | +-- | Format a 'FileData' into a markdown list item. |
| 462 | +multipartFileToItem :: FileData tag -> Text |
| 463 | +multipartFileToItem (FileData name _ contentType _) = |
| 464 | + " - *" <> name <> "*, content-type: " <> "`" <> contentType <> "`" |
| 465 | + |
| 466 | +-- | Format a description and a sample 'MultipartData' into a markdown list |
| 467 | +-- item. |
| 468 | +multipartSampleToDesc |
| 469 | + :: Text -- ^ The description for the sample. |
| 470 | + -> MultipartData tag -- ^ The sample 'MultipartData'. |
| 471 | + -> Text -- ^ A markdown list item. |
| 472 | +multipartSampleToDesc desc (MultipartData inputs files) = |
| 473 | + "- " <> desc <> "\n" <> |
| 474 | + " - textual inputs (any `<input>` type but file):\n" <> |
| 475 | + foldMap (\input -> multipartInputToItem input <> "\n") inputs <> |
| 476 | + " - file inputs (any HTML input that looks like `<input type=\"file\" name=\"somefile\" />`):\n" <> |
| 477 | + foldMap (\file -> multipartFileToItem file <> "\n") files |
| 478 | + |
| 479 | +-- | Format a list of samples generated with 'ToMultipartSample' into sections |
| 480 | +-- of markdown. |
| 481 | +toMultipartDescriptions |
| 482 | + :: forall tag a. |
| 483 | + ToMultipartSample tag a |
| 484 | + => Proxy tag -> Proxy a -> [Text] |
| 485 | +toMultipartDescriptions _ proxyA = fmap (uncurry multipartSampleToDesc) samples |
| 486 | + where |
| 487 | + samples :: [(Text, MultipartData tag)] |
| 488 | + samples = toMultipartSamples proxyA |
| 489 | + |
| 490 | +-- | Create a 'DocNote' that represents samples for this multipart input. |
| 491 | +toMultipartNotes |
| 492 | + :: ToMultipartSample tag a |
| 493 | + => Int -> Proxy tag -> Proxy a -> DocNote |
| 494 | +toMultipartNotes maxSamples' proxyTag proxyA = |
| 495 | + let sampleLines = take maxSamples' $ toMultipartDescriptions proxyTag proxyA |
| 496 | + body = |
| 497 | + [ "This endpoint takes `multipart/form-data` requests. The following is " <> |
| 498 | + "a list of sample requests:" |
| 499 | + , foldMap (<> "\n") sampleLines |
| 500 | + ] |
| 501 | + in DocNote "Multipart Request Samples" $ fmap unpack body |
| 502 | + |
| 503 | +-- | Declare an instance of 'ToMultipartSample' for your 'MultipartForm' type |
| 504 | +-- to be able to use this 'HasDocs' instance. |
| 505 | +instance (HasDocs api, ToMultipartSample tag a) => HasDocs (MultipartForm tag a :> api) where |
| 506 | + docsFor |
| 507 | + :: Proxy (MultipartForm tag a :> api) |
| 508 | + -> (Endpoint, Action) |
| 509 | + -> DocOptions |
| 510 | + -> API |
| 511 | + docsFor _ (endpoint, action) opts = |
| 512 | + let newAction = |
| 513 | + action |
| 514 | + & notes <>~ |
| 515 | + [ toMultipartNotes |
| 516 | + (view maxSamples opts) |
| 517 | + (Proxy :: Proxy tag) |
| 518 | + (Proxy :: Proxy a) |
| 519 | + ] |
| 520 | + in docsFor (Proxy :: Proxy api) (endpoint, newAction) opts |
0 commit comments