packages feed

servant-multipart-api 0.12.1 → 0.13.0

raw patch · 4 files changed

+295/−32 lines, 4 filesdep +deepseqdep +servant-multipart-apidep +tastydep −transformersdep ~basedep ~bytestringdep ~servantnew-uploaderPVP ok

version bump matches the API change (PVP)

Dependencies added: deepseq, servant-multipart-api, tasty, tasty-hunit

Dependencies removed: transformers

Dependency ranges changed: base, bytestring, servant, text

API changes (from Hackage documentation)

- Servant.Multipart.API: instance Servant.Links.HasLink sub => Servant.Links.HasLink (Servant.Multipart.API.MultipartForm tag a Servant.API.Sub.:> sub)
+ Servant.Multipart.API: instance Control.DeepSeq.NFData (Servant.Multipart.API.MultipartResult tag) => Control.DeepSeq.NFData (Servant.Multipart.API.FileData tag)
+ Servant.Multipart.API: instance Control.DeepSeq.NFData (Servant.Multipart.API.MultipartResult tag) => Control.DeepSeq.NFData (Servant.Multipart.API.MultipartData tag)
+ Servant.Multipart.API: instance Control.DeepSeq.NFData Servant.Multipart.API.Input
+ Servant.Multipart.API: instance GHC.Base.Monoid (Servant.Multipart.API.MultipartData tag)
+ Servant.Multipart.API: instance GHC.Base.Semigroup (Servant.Multipart.API.MultipartData tag)
+ Servant.Multipart.API: instance GHC.Classes.Eq (Servant.Multipart.API.MultipartResult tag) => GHC.Classes.Eq (Servant.Multipart.API.MultipartData tag)
+ Servant.Multipart.API: instance GHC.Show.Show (Servant.Multipart.API.MultipartResult tag) => GHC.Show.Show (Servant.Multipart.API.MultipartData tag)
+ Servant.Multipart.API: instance Servant.Links.HasLink sub => Servant.Links.HasLink (Servant.Multipart.API.MultipartForm' mods tag a Servant.API.Sub.:> sub)
+ Servant.Multipart.API: lookupAllFiles :: Text -> MultipartData tag -> [FileData tag]
+ Servant.Multipart.API: lookupAllInputs :: Text -> MultipartData tag -> [Text]
+ Servant.Multipart.API: lookupAllInputsAs :: FromHttpApiData a => Text -> MultipartData tag -> Either String [a]
+ Servant.Multipart.API: lookupFile :: Text -> MultipartData tag -> Either String (FileData tag)
+ Servant.Multipart.API: lookupInput :: Text -> MultipartData tag -> Either String Text
+ Servant.Multipart.API: lookupInputAs :: FromHttpApiData a => Text -> MultipartData tag -> Either String a
- Servant.Multipart.API: data MultipartForm' (mods :: [*]) tag a
+ Servant.Multipart.API: data MultipartForm' (mods :: [Type]) tag a
- Servant.Multipart.API: type MultipartForm tag a = MultipartForm' '[] tag a
+ Servant.Multipart.API: type MultipartForm tag a = MultipartForm' '[] :: [Type] tag a
- Servant.Multipart.API: type family MultipartResult tag :: *
+ Servant.Multipart.API: type family MultipartResult tag

Files

CHANGELOG.md view
@@ -1,3 +1,20 @@+0.13.0+------++- Add `lookupInput` and `lookupFile`, moved from `servant-multipart`.+- Add `lookupAllInputs`, `lookupAllFiles`, `lookupInputAs` and+  `lookupAllInputsAs`+  [#75](https://github.com/haskell-servant/servant-multipart/pull/75).+- Add `Eq`, `Show`, `Semigroup` and `Monoid` instances for+  `MultipartData`, and `NFData` instances for `MultipartData`,+  `FileData` and `Input`.+- **Breaking:** the `HasLink` instance now covers `MultipartForm'` with any+  modifiers, not only `MultipartForm`. Remove any instances you wrote for+  `MultipartForm' '[Lenient]`, since they now overlap.+- Drop the `transformers` dependency.+- Require GHC >= 9.4 and servant >= 0.20.3; support up to GHC 9.12+  [#81](https://github.com/haskell-servant/servant-multipart/pull/81).+ 0.12.1 ------ 
servant-multipart-api.cabal view
@@ -1,5 +1,5 @@ name:               servant-multipart-api-version:            0.12.1+version:            0.13.0 synopsis:           multipart/form-data (e.g file upload) support for servant description:   This package contains servant API types that support multiform upload, used by@@ -15,7 +15,12 @@ build-type:         Simple cabal-version:      >=1.10 extra-source-files: CHANGELOG.md-tested-with: GHC ==8.0.2 || ==8.2.2 || ==8.4.4 || ==8.6.5 || ==8.8.4 || ==8.10.4+tested-with:+  GHC ==9.4.8+   || ==9.6.3+   || ==9.8.4+   || ==9.10.3+   || ==9.12.4  library   default-language: Haskell2010@@ -24,14 +29,26 @@    -- ghc boot libs   build-depends:-      base          >=4.9      && <5-    , bytestring    >=0.10.8.1 && <0.11-    , text          >=1.2.3.0  && <1.3-    , transformers  >=0.5.2.0  && <0.6+      base          >=4.17     && <5+    , bytestring    >=0.11     && <0.13+    , deepseq       >=1.4.8    && <1.6+    , text          >=2.0      && <2.2    -- other dependencies   build-depends:-      servant             >=0.16     && <0.19+      servant             >=0.20.3   && <0.21++test-suite servant-multipart-api-test+  type:             exitcode-stdio-1.0+  hs-source-dirs:   test+  main-is:          Test.hs+  default-language: Haskell2010+  build-depends:+      base+    , servant-multipart-api+    , tasty+    , tasty-hunit+    , text  source-repository head   type:     git
src/Servant/Multipart/API.hs view
@@ -1,4 +1,3 @@-{-# LANGUAGE CPP #-} {-# LANGUAGE AllowAmbiguousTypes #-} {-# LANGUAGE DataKinds #-} {-# LANGUAGE TypeFamilies #-}@@ -31,9 +30,18 @@   , Mem   , Input(..)   , FileData(..)+  , lookupInput+  , lookupFile+  , lookupAllInputs+  , lookupAllFiles+  , lookupInputAs+  , lookupAllInputsAs   ) where -import Data.Text (Text)+import Control.DeepSeq (NFData (rnf))+import Data.Bifunctor (first)+import Data.List (find)+import Data.Text (Text, unpack) import Data.Typeable import Servant.API @@ -50,11 +58,11 @@ --   stand now. This also means that 'MultipartForm' can't be used in --   conjunction with 'ReqBody' in an endpoint. -----   The 'tag' type parameter instructs the function to handle data+--   The @tag@ type parameter instructs servant-multipart to handle data --   either as data to be saved to temporary storage ('Tmp') or saved to --   memory ('Mem'). -----   The 'a' type parameter represents the Haskell type to which+--   The @a@ type parameter represents the Haskell type to which --   you are going to decode the multipart data to, where the --   multipart data consists in all the usual form inputs along --   with the files sent along through @\<input type="file"\>@@@ -103,33 +111,47 @@ --   server :: User -> Handler String --   server usr = return str -----     where str = username usr ++ "'s profile picture"+--     where str = unpack (username usr) ++ "'s profile picture" --              ++ " got temporarily uploaded to " --              ++ pic usr ++ " and will be removed from there " --              ++ " after this handler has run." --   @ -- --   Note that the behavior of this combinator is configurable,---   by using 'serveWith' from servant-server instead of 'serve',---   which takes an additional 'Context' argument. It simply is an---   heterogeneous list where you can for example store---   a value of type 'MultipartOptions' that has the configuration that---   you want, which would then get picked up by servant-multipart.+--   by using @serveWithContext@ from servant-server instead of @serve@,+--   which takes an additional @Context@ argument. @Context@ is a+--   heterogeneous list where you can for example store a value of type+--   @MultipartOptions@ from servant-multipart that has the configuration+--   that you want, which would then get picked up by servant-multipart. -----   __Important__: as mentionned in the example above,+--   __Important__: as mentioned in the example above, --   the file paths point to temporary files which get removed --   after your handler has run, if they are still there. It is --   therefore recommended to move or copy them somewhere in your --   handler code if you need to keep the content around.+--+--   __Important__: the input names, input values, file names and file+--   content types of the submitted form must all be valid UTF-8, including+--   those of the parts that your 'FromMultipart' instance ignores. A form+--   that carries any other encoding is rejected with a 400 response before+--   your handler runs, unless 'Servant.API.Modifiers.Lenient' is used. The+--   contents of the uploaded files are not decoded and may be arbitrary+--   bytes. type MultipartForm tag a = MultipartForm' '[] tag a  -- | 'MultipartForm' which can be modified with 'Servant.API.Modifiers.Lenient'.+--+--   Under 'Servant.API.Modifiers.Lenient', the handler is passed an+--   @'Either' CheckError a@ from servant-multipart rather than the request+--   being rejected, so it is handed the message from a failed+--   'fromMultipart' call, from a form whose text is not valid UTF-8, or+--   from a form that exceeds a body parsing limit. data MultipartForm' (mods :: [*]) tag a  -- | What servant gets out of a @multipart/form-data@ form submission. -----   The type parameter 'tag' tells if 'MultipartData' is stored as a---   temporary file or stored in memory. 'tag' is type of either 'Mem'+--   The type parameter @tag@ tells if 'MultipartData' is stored as a+--   temporary file or stored in memory. @tag@ is type of either 'Mem' --   or 'Tmp'. -- --   The 'inputs' field contains a list of textual 'Input's, where@@ -147,6 +169,130 @@   , files  :: [FileData tag]   } +deriving instance Eq (MultipartResult tag) => Eq (MultipartData tag)+deriving instance Show (MultipartResult tag) => Show (MultipartData tag)++instance Semigroup (MultipartData tag) where+  a <> b =+    MultipartData+      { inputs = inputs a <> inputs b+      , files  = files a <> files b+      }++instance Monoid (MultipartData tag) where+  mempty =+    MultipartData+      { inputs = []+      , files  = []+      }++instance NFData (MultipartResult tag) => NFData (MultipartData tag) where+  rnf (MultipartData is fs) = rnf is `seq` rnf fs++-- | Lookup a textual input with the given @name@ attribute.+--+-- Takes linear time with respect to the number of inputs.+lookupInput :: Text -> MultipartData tag -> Either String Text+lookupInput iname =+  maybe (Left $ "Field " <> unpack iname <> " not found") (Right . iValue)+  . find ((==iname) . iName)+  . inputs++-- | Lookup a file input with the given @name@ attribute.+--+-- Takes linear time with respect to the number of files.+lookupFile :: Text -> MultipartData tag -> Either String (FileData tag)+lookupFile iname =+  maybe (Left $ "File " <> unpack iname <> " not found") Right+  . find ((==iname) . fdInputName)+  . files++-- | Lookup all textual inputs with the given @name@ attribute.+-- +-- Takes linear time with respect to the number of inputs.+--+-- This function returns a list of all values for inputs with the specified name.+-- It is useful when handling forms that allow multiple inputs with the same name,+-- such as multiple select inputs or checkbox groups with explicit values. +--+-- Example:+--+-- @+-- let mpd = MultipartData [Input "color" "red", Input "color" "blue"] []+-- lookupAllInputs "color" mpd == ["red", "blue"]+-- lookupAllInputs "size"  mpd == []+-- @+lookupAllInputs :: Text -> MultipartData tag -> [Text]+lookupAllInputs iname mpd = [ val | (Input name val) <- inputs mpd, name == iname ]++-- | Lookup all file inputs with the given @name@ attribute.+--+-- Takes linear time with respect to the number of files.+--+-- This function returns a list of all files uploaded under the specified name.+-- It is useful when handling forms that allow multiple file uploads with the same+-- name, such as file inputs with the @multiple@ attribute. +--+-- Example:+--+-- @+-- let file1 = FileData "file" "doc1.pdf" "application/pdf" "/tmp/doc1"+--     file2 = FileData "file" "doc2.pdf" "application/pdf" "/tmp/doc2"+--     mpd   = MultipartData [] [file1, file2] :: MultipartData Tmp+-- lookupAllFiles "file"  mpd == [file1, file2]+-- lookupAllFiles "image" mpd == []+-- @+lookupAllFiles :: Text -> MultipartData tag -> [FileData tag]+lookupAllFiles iname mpd = [ f | f <- files mpd, fdInputName f == iname ]++-- | Lookup a textual input with the given @name@ attribute and parse it into the desired type.+--+-- Takes linear time with respect to the number of inputs.+--+-- This function returns the parsed value if the input exists and can be parsed successfully+-- using its 'FromHttpApiData' instance. If the input is not found or parsing fails, it returns+-- an error message.+--+-- Note: This function requires the field to be present in the request. Standalone HTML boolean+-- checkboxes (which submit @"on"@ when checked and are omitted by browsers when unchecked) are+-- not directly supported by 'FromHttpApiData Bool'; check for presence with 'lookupInput' or+-- 'lookupAllInputs' instead, or use a custom newtype with a 'FromHttpApiData' instance.+--+-- Example:+--+-- @+-- let mpd = MultipartData [Input "age" "30"] []+-- lookupInputAs "age"     mpd == Right (30 :: Int)+-- lookupInputAs "isAdmin" mpd == (Left "Field isAdmin not found" :: Either String Int)+-- @+lookupInputAs :: FromHttpApiData a => Text -> MultipartData tag -> Either String a+lookupInputAs iname mpd = do+  val <- lookupInput iname mpd+  first unpack $ parseQueryParam val++-- | Lookup all textual inputs with the given @name@ attribute and parse them into the desired type.+--+-- Takes linear time with respect to the number of inputs.+--+-- This function returns a list of parsed values for inputs with the specified name using their+-- 'FromHttpApiData' instance. It is useful for forms with repeated fields, multiple select inputs,+-- or checkbox groups sharing the same name with explicit values.+--+-- If no inputs are found, an empty list is returned. If parsing fails for any value,+-- an error message is returned.+--+-- Example:+--+-- @+-- let mpd = MultipartData [Input "nums" "1", Input "nums" "2"] []+-- lookupAllInputsAs "nums" mpd == Right [1, 2 :: Int]+-- lookupAllInputsAs "size" mpd == Right ([] :: [Int])+-- @+lookupAllInputsAs :: FromHttpApiData a => Text -> MultipartData tag -> Either String [a]+lookupAllInputsAs iname mpd = do+  let vals = lookupAllInputs iname mpd+  first unpack $ mapM parseQueryParam vals+ -- | Representation for an uploaded file, usually resulting from --   picking a local file for an HTML input that looks like --   @\<input type="file" name="somefile" /\>@.@@ -166,6 +312,13 @@ deriving instance Eq (MultipartResult tag) => Eq (FileData tag) deriving instance Show (MultipartResult tag) => Show (FileData tag) +-- | Note that at 'Tmp' this only forces the 'FilePath'. It makes no+--   guarantees about the temporary file it names, which is still removed+--   once the handler has run.+instance NFData (MultipartResult tag) => NFData (FileData tag) where+  rnf (FileData iname fname ctype payload) =+    rnf iname `seq` rnf fname `seq` rnf ctype `seq` rnf payload+ -- | Representation for a textual input (any @\<input\>@ type but @file@). -- --   @\<input name="foo" value="bar"\ />@ would appear as @'Input' "foo" "bar"@.@@ -174,6 +327,9 @@   , iValue :: Text -- ^ value given for that input   } deriving (Eq, Show) +instance NFData Input where+  rnf (Input name value) = rnf name `seq` rnf value+ -- | 'MultipartData' is the type representing --   @multipart/form-data@ form inputs. Sometimes --   you may instead want to work with a more structured type@@ -188,8 +344,8 @@ -- --   instance FromMultipart Tmp User where --     fromMultipart form =---       User \<$\> lookupInput "username" (inputs form)---            \<*\> fmap fdPayload (lookupFile "pic" $ files form)+--       User \<$\> lookupInput "username" form+--            \<*\> fmap fdPayload (lookupFile "pic" form) --   @ class FromMultipart tag a where   -- | Given a value of type 'MultipartData', which consists@@ -208,16 +364,16 @@ --   @ --   data User = User { username :: Text, pic :: FilePath } -----   instance toMultipart Tmp User where+--   instance ToMultipart Tmp User where --       toMultipart user = MultipartData [Input "username" $ username user] --                                        [FileData "pic"---                                                  (pic user)+--                                                  (pack (takeFileName (pic user))) --                                                  "image/png" --                                                  (pic user) --                                        ] --   @ class ToMultipart tag a where-  -- | Given a value of type 'a', convert it to a+  -- | Given a value of type @a@, convert it to a   -- 'MultipartData'.   toMultipart :: a -> MultipartData tag @@ -234,11 +390,6 @@ type instance MultipartResult Tmp = FilePath type instance MultipartResult Mem = LBS.ByteString -instance HasLink sub => HasLink (MultipartForm tag a :> sub) where-#if MIN_VERSION_servant(0,14,0)-  type MkLink (MultipartForm tag a :> sub) r = MkLink sub r+instance HasLink sub => HasLink (MultipartForm' mods tag a :> sub) where+  type MkLink (MultipartForm' mods tag a :> sub) r = MkLink sub r   toLink toA _ = toLink toA (Proxy :: Proxy sub)-#else-  type MkLink (MultipartForm tag a :> sub) = MkLink sub-  toLink _ = toLink (Proxy :: Proxy sub)-#endif
+ test/Test.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeApplications #-}+module Main (main) where++import Data.Text (Text)+import Test.Tasty+import Test.Tasty.HUnit++import Servant.Multipart.API++main :: IO ()+main = defaultMain lookupTests++sampleFileData :: FileData Tmp+sampleFileData = FileData "file" "doc1.pdf" "application/pdf" "payload1"++lookupTests :: TestTree+lookupTests = testGroup "Lookup function tests"+  [ testCase "lookupInput - found" $ do+       let md = MultipartData [ Input "title" "hello" ] []+       lookupInput "title" md @?= Right "hello"++  , testCase "lookupInput - missing" $ do+       let md = MultipartData [ Input "title" "hello" ] []+       lookupInput "author" md @?= Left "Field author not found"++  , testCase "lookupFile - found" $ do+       let md = MultipartData [] [ sampleFileData ]+       lookupFile "file" md @?= Right sampleFileData++  , testCase "lookupFile - missing" $ do+       let md = MultipartData [] [ sampleFileData ]+       lookupFile "image" md @?= Left "File image not found"++  , testCase "lookupAllInputs - found" $ do+       let md = MultipartData [ Input "color" "red"+                              , Input "color" "blue"+                              ] []+       lookupAllInputs "color" md @?= ["red", "blue"]++  , testCase "lookupAllInputs - missing field" $ do+       let md = MultipartData [ Input "color" "red"+                              , Input "color" "blue"+                              ] []+       lookupAllInputs "size" md @?= []++  , testCase "lookupAllFiles - found" $ do+       let file1 = sampleFileData+           file2 = FileData "file" "doc2.pdf" "application/pdf" "/tmp/doc2.buf" :: FileData Tmp+           md = MultipartData [] [file1, file2]+       lookupAllFiles "file" md @?= [file1, file2]++  , testCase "lookupAllFiles - missing file" $ do+       let file1 = sampleFileData+           md = MultipartData [] [file1]+       lookupAllFiles "image" md @?= []++  , testCase "lookupInputAs - parsed successfully" $ do+       let md = MultipartData [ Input "age" "30" ] []+       lookupInputAs @Int "age" md @?= Right 30++  , testCase "lookupInputAs - missing input" $ do+       let md = MultipartData [ Input "age" "30" ] []+       case lookupInputAs @Bool "isAdmin" md of+         Left err -> err @?= "Field isAdmin not found"+         Right _  -> assertFailure "Expected failure on missing input"++  , testCase "lookupAllInputsAs - parsing list of numbers" $ do+       let md = MultipartData [ Input "nums" "1"+                              , Input "nums" "2"+                              , Input "nums" "3"+                              ] []+       lookupAllInputsAs @Int "nums" md @?= Right [1,2,3]++  , testCase "lookupAllInputsAs - missing field returns empty list" $ do+       let md = MultipartData [ Input "nums" "1" ] []+       lookupAllInputsAs @Int "other" md @?= Right []+  ]