packages feed

digestive-functors 0.0.2.1 → 0.1.0.0

raw patch · 7 files changed

+316/−104 lines, 7 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Text.Digestive.Cli: Descriptions :: Map FormId [String] -> Descriptions
- Text.Digestive.Cli: instance Monoid Descriptions
- Text.Digestive.Cli: instance Show Descriptions
- Text.Digestive.Cli: newtype Descriptions
- Text.Digestive.Cli: unDescriptions :: Descriptions -> Map FormId [String]
- Text.Digestive.Result: FormId :: String -> Integer -> FormId
- Text.Digestive.Result: formId :: FormId -> Integer
- Text.Digestive.Result: formPrefix :: FormId -> String
+ Text.Digestive.Cli: instance Monoid PromptView
+ Text.Digestive.Cli: instance Show FieldItem
+ Text.Digestive.Cli: instance Show PromptView
+ Text.Digestive.Cli: promptList :: String -> Prompt a -> Prompt [a]
+ Text.Digestive.Forms: getInputStrings :: FormInput i f => i -> [String]
+ Text.Digestive.Forms: getInputTexts :: FormInput i f => i -> [Text]
+ Text.Digestive.Forms: inputList :: (Monad m, Monoid v) => Formlet m i e v Int -> Formlet m i e v a -> Formlet m i e v [a]
+ Text.Digestive.Forms.Html: instance Functor FormHtml
+ Text.Digestive.Result: formIdList :: FormId -> [Integer]
+ Text.Digestive.Result: getResult :: Result e ok -> Maybe ok
+ Text.Digestive.Result: mapId :: ([Integer] -> [Integer]) -> FormId -> FormId
+ Text.Digestive.Result: unitRange :: FormId -> FormRange
+ Text.Digestive.Result: zeroId :: String -> FormId
+ Text.Digestive.Transform: transformFormlet :: Monad m => (b -> a) -> Formlet m i e v a -> Transformer m e a b -> Formlet m i e v b
+ Text.Digestive.Types: bracketState :: Monad m => FormState m i a -> FormState m i a
+ Text.Digestive.Types: getFormInput' :: Monad m => FormId -> FormState m i (Maybe i)
+ Text.Digestive.Types: incState :: Monad m => FormState m i ()
+ Text.Digestive.Types: type Formlet m i e v a = Maybe a -> Form m i e v a
- Text.Digestive.Cli: runPrompt :: Prompt a -> IO a
+ Text.Digestive.Cli: runPrompt :: Prompt a -> IO (Either [String] a)
- Text.Digestive.Cli: type Prompt a = Form IO String String Descriptions a
+ Text.Digestive.Cli: type Prompt a = Form IO String String PromptView a

Files

digestive-functors.cabal view
@@ -1,5 +1,5 @@ Name:                digestive-functors-Version:             0.0.2.1+Version:             0.1.0.0 Synopsis:            A general way to consume input using applicative functors Description:         Digestive functors is a library to generate and process     HTML forms.  You can find an introduction here:
src/Text/Digestive/Cli.hs view
@@ -3,114 +3,194 @@ -- {-# LANGUAGE GeneralizedNewtypeDeriving #-} module Text.Digestive.Cli-    ( Descriptions (..)-    , Prompt+    ( Prompt     , prompt+    , promptList     , promptRead     , runPrompt     ) where -import Data.Map (Map)-import qualified Data.Map as M-import Data.Monoid (Monoid, mempty, mappend)+import Data.Monoid (Monoid, mappend, mempty) import Control.Applicative ((<$>))  import Text.Digestive.Result import Text.Digestive.Types import Text.Digestive.Transform-import qualified Text.Digestive.Common as Common+import Text.Digestive.Forms (inputList) --- | A list of descriptions for a certain input prompt+-- A representation of an element in the structure used to gather inputs+-- from the user. ---newtype Descriptions = Descriptions-    { unDescriptions :: Map FormId [String]-    } deriving (Show)--instance Monoid Descriptions where-    mempty = Descriptions mempty-    mappend (Descriptions m1) (Descriptions m2) =-        Descriptions $ M.unionWith (++) m1 m2+data FieldItem+  -- A tangible item that the user is prompted for.+  = FieldItemSingle     FormId String [String]+  -- A delimter marking the start of a series of prompts to be entered+  -- multiple times.+  | FieldItemMultiStart FormId String [String]+  -- A delimiter marking the end of a multiple input prompt.+  | FieldItemMultiEnd+    deriving (Show) --- | Type for a prompt+-- The structure of a prompt, built up as a View. ---type Prompt a = Form IO String String Descriptions a+newtype PromptView = PromptView+    { unPromptView :: [FieldItem]+    } deriving (Show, Monoid) --- | Remove the descriptions for the inputs already in the input map.+-- | Type for a prompt ---neededDescriptions :: InputMap -> Descriptions -> Descriptions-neededDescriptions (InputMap inputMap) =-    Descriptions . M.filterWithKey notInInput . unDescriptions-  where-    notInInput k _ = k `notElem` map fst inputMap+type Prompt a = Form IO String String PromptView a --- | Add errors to the descriptions+-- An association list of FormIds and their inputs gathered by prompting the+-- user. ---addErrors :: [(FormRange, String)] -> Descriptions -> Descriptions-addErrors errors (Descriptions descr) = Descriptions $ foldl add' descr errors-  where-    add' map' ((FormRange x _, e)) = M.insertWith (++) x [e] map'- newtype InputMap = InputMap     { unInputMap :: [(FormId, String)]     } deriving (Show, Monoid) --- | Create an environment from an input map+-- Create an environment from an input map -- inputMapEnvironment :: Monad m => InputMap -> Environment m String inputMapEnvironment map' = Environment $ return . flip lookup (unInputMap map') --- | Prompt once for a needed field. Return (key, value)----promptOnce :: Descriptions -> IO (FormId, String)-promptOnce (Descriptions descr)-    | M.null descr = error "No descriptions!"-    | otherwise = do putStrLn ""-                     mapM_ putStrLn description-                     putStr "> "-                     (,) key <$> getLine-  where-    (key, description) = M.findMin descr---- | Remove all input for which errors are found+-- | Generate a prompt field for a String ---removeInvalidInput :: InputMap -> [(FormRange, String)] -> InputMap-removeInvalidInput = foldl removeInvalidInput'-  where-    removeInvalidInput' :: InputMap -> (FormRange, String) -> InputMap-    removeInvalidInput' (InputMap map') (range, _) =-        InputMap $ filter (not . flip isInRange range . fst) map'+prompt :: String        -- ^ Description+       -> Prompt String -- ^ Resulting prompt+prompt descr = Form $ do+    id' <- getFormId+    inp <- getFormInput+    range <- getFormRange+    let v :: [(FormRange, String)] -> PromptView+        v errs = PromptView [FieldItemSingle id' descr matching]+          where+            -- Only errors which apply specifically to this item+            matching = retainErrors range errs+        result = case inp of+            Just x  -> Ok x+            Nothing -> Error [(range, "No input")]+    return (View v, result) --- | Generate a prompt field from a description+-- | Convert a prompt for a single item into a prompt for multiple items ---prompt :: String -> Prompt String-prompt descr = Common.input (const $ const $ const [])-                            toResult-                            (\x _ -> Descriptions $ M.singleton x [descr])-                            ""+promptList :: String     -- ^ Description of resulting multi-prompt+           -> Prompt a   -- ^ Prompt to convert+           -> Prompt [a] -- ^ Resulting multiple input prompt+promptList descr prmpt = Form $ do+    id'     <- getFormId+    (v, r1) <- unForm $ inputList numPrompt (const prmpt) Nothing+    range   <- getFormRange+    -- The monoid for the view will look like this: [vstart, v, vend]+    -- 'vstart' and 'vend' delimit the beginning and end of the inputs that+    -- are converted into repeatable inputs. When we are prompting the user to+    -- fill out the form, we use these delimiters to control the behavior of+    -- the prompts. Anything between them will be treated as repeatable. The+    -- 'vstart' item (FieldItemMultiStart) also contains the FormId of the+    -- field used to count the number of entries, as well as an additional+    -- description of of the multiple input itself (for example, 'Users', when+    -- the contained items are used to enter in a single User.)+    let vstart errs = PromptView [item]+          where+            item = FieldItemMultiStart id' descr $ retainErrors range errs+        vend _ = PromptView [FieldItemMultiEnd]+    return (View vstart `mappend` v `mappend` View vend, r1)   where-    toResult Nothing = Error []-    toResult (Just x) = Ok x+    numPrompt _ = Form $ do+                    inp <- getFormInput+                    return (mempty, readN inp)+    readN (Just x) = Ok (read x)+    readN Nothing = Error []  -- | Generate a prompt field for a value which can be read -- promptRead :: Read a-           => String    -- ^ Error when the value can't be read-           -> String    -- ^ Description-           -> Prompt a  -- ^ Resulting prompt+           => String   -- ^ Error when the value can't be read+           -> String   -- ^ Description+           -> Prompt a -- ^ Resulting prompt promptRead error' descr = prompt descr `transform` transformRead error' --- | Run a prompt. This will repeatedly prompt for input until we have a valid--- result.+-- Get a single line of text from the user ---runPrompt :: Prompt a -> IO a-runPrompt form = prompt' mempty+cliInput :: IO String+cliInput = putStr "> " >> getLine++-- Get the input for a list of prompt items that have been defined.+--+-- Notably, this supports nested 'mass input' forms (inputList/promptList)+-- which are delimited by FieldItemMultiStart and FieldItemMultiEnd.+-- FieldItemMultiSingle represents a tangible item to prompt for. If a+-- FieldItemMultiStart is reached, we need to prompt for all of the items+-- until the next FieldItemMultiEnd an arbitrary number of times.+--+inputForItems :: [FieldItem]+              -- ^ Items to get input for+              -> [(FormId, String)]+              -- ^ Accumulated association list of inputs we've prompted for+              -> (FormId -> FormId)+              -- ^ A function to transform the FormId of this item. Used to+              -- change the index of the item when prompting multiple times.+              -> IO ([FieldItem], [(FormId, String)])+              -- ^ A pair of the remaining items (empty if we are not+              -- returning from a multiple input item) and accumulated inputs, +inputForItems [] accum _fid = return ([], accum)++inputForItems (FieldItemMultiEnd : rest) accum _fid = return (rest, accum)++-- The simple case for a single item.+inputForItems (FieldItemSingle id' descr _errs : rest) accum fid = do+    putStrLn descr+    val <- cliInput+    inputForItems rest ((fid id', val) : accum) fid++-- The case for a multiple input prompt.+inputForItems (FieldItemMultiStart id' descr _errs : rest) accum fid = do+    let id'' = fid id'+    putStrLn $ "How many '" ++ descr ++ "' do you want to input?"+    -- Leave this as a string, since we must put it into a hidden form field+    -- for inputList, which must read it again. We only prompt for it here+    -- instead of as a discrete form item because we want to know, right now,+    -- how many the user wants to input.+    nStr <- cliInput+    -- Prompt for all of the delimited items, and put them at index i for this+    -- multi-input item.+    -- TODO use foldM+    let f i = do putStrLn $ descr ++ " #" ++ show (i + 1) ++ ":"+                 inputForItems rest [] (modifyId id'' i)+    delimited <- mapM f [0..(read nStr - 1)]+    let rest' = fst $ last delimited+        countfield = (id'', nStr)+    inputForItems rest' ([countfield] ++ accum ++ concatMap snd delimited) fid++-- Construct a function to transform the 'children' (delimited items) of a+-- mass input item to the correct index.+--+modifyId :: FormId -> Integer -> FormId -> FormId+modifyId parent i = mapId (\x -> head x : i : formIdList parent)++-- | Run a Prompt, sequentially prompting the user for each item.+--+runPrompt :: Prompt a+          -- ^ The Prompt to run+          -> IO (Either [String] a)+          -- ^ A list of error strings, or the result.+runPrompt prmpt = do+    prmptv <- viewForm prmpt "form"+    inpmap <- InputMap . snd <$> inputForItems (unPromptView prmptv) [] id+    eith   <- eitherForm prmpt "form" (inputMapEnvironment inpmap)+    return $ case eith of+        Left v  -> Left (fieldItemErrors `concatMap` unPromptView v)+        Right x -> Right x++-- Read the errors from a FieldItem, if any.+--+fieldItemErrors :: FieldItem -> [String]+fieldItemErrors (FieldItemSingle id' descr errs) =+    descriptiveErrors id' descr errs+fieldItemErrors (FieldItemMultiStart id' descr errs) = -- TODO bad+    descriptiveErrors id' descr errs+fieldItemErrors FieldItemMultiEnd = []++descriptiveErrors :: FormId -> String -> [String] -> [String]+descriptiveErrors id' descr errs = map str errs   where-    prompt' inputMap = do-        (v, r) <- runForm form "form" $ inputMapEnvironment inputMap-        case r of-            Ok x -> return x-            Error e -> do let inputMap' = removeInvalidInput inputMap e-                              descr = addErrors e-                                    $ neededDescriptions inputMap' (unView v [])-                          input' <- promptOnce descr-                          prompt' $ inputMap' `mappend` InputMap [input']+    str err = "(" ++ show id' ++ ") " ++ descr ++ ": " ++ err
src/Text/Digestive/Forms.hs view
@@ -1,4 +1,5 @@-{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies #-}+{-# LANGUAGE MultiParamTypeClasses, FunctionalDependencies,+             NoMonomorphismRestriction #-} module Text.Digestive.Forms     ( FormInput (..)     , inputString@@ -6,12 +7,14 @@     , inputBool     , inputChoice     , inputFile+    , inputList     ) where  import Control.Applicative ((<$>)) import Control.Monad (mplus)-import Data.Monoid (Monoid, mconcat)-import Data.Maybe (fromMaybe)+import Control.Monad.State (put, get)+import Data.Monoid (Monoid, mappend, mconcat)+import Data.Maybe (fromMaybe, listToMaybe)  import Data.Text (Text) import qualified Data.Text as T (pack)@@ -29,13 +32,22 @@     -- among other things     --     getInputString :: i -> Maybe String+    getInputString = listToMaybe . getInputStrings -    -- | Parse the input value into 'Text'. The default implementation uses-    -- 'T.pack . getInputString', a more efficient version may be implemented.+    -- | Should be implemented     --+    getInputStrings :: i -> [String]++    -- | Parse the input value into 'Text'+    --     getInputText :: i -> Maybe Text-    getInputText = fmap T.pack . getInputString+    getInputText = listToMaybe . getInputTexts +    -- | Can be overriden for efficiency concerns+    --+    getInputTexts :: i -> [Text]+    getInputTexts = map T.pack . getInputStrings+     -- | Get a file descriptor for an uploaded file     --     getInputFile :: i -> Maybe f@@ -46,7 +58,7 @@             -> Form m i e v String            -- ^ Resulting form inputString = input toView toResult   where-    toView _ inp defaultInput = (getInputString =<< inp) `mplus` defaultInput+    toView _ inp def = (getInputString =<< inp) `mplus` def     toResult = Ok . fromMaybe "" . (getInputString =<<)  inputRead :: (Monad m, Functor m, FormInput i f, Read a, Show a)@@ -63,11 +75,12 @@           -> Form m i e v Bool       -- ^ Resulting form inputBool = input toView toResult   where-    toView isInput inp def = if isInput then readBool (getInputString =<< inp)-                                        else def+    toView isInput inp def+        | isInput   = readBool (getInputString =<< inp)+        | otherwise = def     toResult inp = Ok $ readBool (getInputString =<< inp)     readBool (Just x) = not $ null x-    readBool Nothing  = False+    readBool _        = False  inputChoice :: (Monad m, Functor m, FormInput i f, Monoid v, Eq a)             => (FormId -> String -> Bool -> a -> v)  -- ^ Choice constructor@@ -94,3 +107,59 @@     toView _ _ _ = ()     toResult inp = Ok $ getInputFile =<< inp     viewCons' id' () = viewCons id'++up :: Monad m => Int -> FormState m i ()+up n = do+    FormRange s _ <- get+    put $ unitRange $ mapId ((!!n) . iterate tail) s++down :: Monad m => Int -> FormState m i ()+down n = do+    FormRange s _ <- get+    put $ unitRange $ mapId ((!!n) . iterate (0:)) s++-- | Converts a formlet representing a single item into a formlet representing a+-- dynamically sized list of those items.  It requires that the user specify a+-- formlet to hold the length of the list.  Typically this will be a hidden+-- field that is automatically updated by client-side javascript.+--+-- The field names must be generated as follows.  Assume that if inputList had+-- not been used, the field name would have been prefix-f5.  In this case, the+-- list length field name will be prefix-f5.  The first item in the list will+-- receive field names starting at prefix-f5.0.0.  If each item is a composed+-- form with two fields, those fields will have the names prefix-f5.0.0 and+-- prefix-f5.0.1.  The field names for the second item will be prefix-f5.1.0+-- and prefix-f5.1.1.+--+inputList :: (Monad m, Monoid v)+          => Formlet m i e v Int  -- ^ A formlet for the list length+          -> Formlet m i e v a    -- ^ The formlet for a single list element+          -> Formlet m i e v [a]  -- ^ The dynamic list formlet+inputList countField single defaults = Form $ do+    let defCount = maybe 1 length defaults+    (countView,countRes) <- unForm $ countField (Just defCount)+    let countFromForm = getResult countRes+        count = fromMaybe defCount countFromForm+        fs = replicate count single+        forms = zipWith ($) fs $ maybe (maybe [Nothing] (map Just) defaults)+                                       (flip replicate Nothing)+                                       countFromForm+    down 2+    list <- mapM (incAfter . unForm) forms+    up 2+    return ( countView `mappend` (mconcat $ map fst list)+           , combineResults [] [] $ map snd list)+  where+    incAfter k = do+        res <- k+        up 1 >> incState >> down 1+        return res++    combineResults es os [] =+        case es of+            [] -> Ok $ reverse os+            _  -> Error es+    combineResults es os (r:rs) =+        case r of+            Error es' -> combineResults (es ++ es') os rs+            Ok o      -> combineResults es (o:os) rs
src/Text/Digestive/Forms/Html.hs view
@@ -57,6 +57,9 @@     mappend (FormHtml x f) (FormHtml y g) =         FormHtml (x `mappend` y) $ f `mappend` g +instance Functor FormHtml where+    fmap f (FormHtml e g) = FormHtml e (f . g)+ -- | Create form HTML with the default encoding type -- createFormHtml :: (FormHtmlConfig -> a) -> FormHtml a
src/Text/Digestive/Result.hs view
@@ -2,9 +2,14 @@ -- module Text.Digestive.Result     ( Result (..)-    , FormId (..)+    , getResult+    , FormId+    , zeroId+    , mapId+    , formIdList     , FormRange (..)     , incrementFormId+    , unitRange     , isInRange     , isSubRange     , retainErrors@@ -12,6 +17,7 @@     ) where  import Control.Applicative (Applicative (..))+import Data.List (intercalate)  -- | Type for failing computations --@@ -35,16 +41,37 @@     Ok _ <*> Error y = Error y     Ok x <*> Ok y = Ok $ x y +getResult :: Result e ok -> Maybe ok+getResult (Error _) = Nothing+getResult (Ok r) = Just r+ -- | An ID used to identify forms -- data FormId = FormId-    { formPrefix :: String-    , formId     :: Integer+    { -- | Global prefix for the form+      formPrefix :: String+    , -- | Stack indicating field. Head is most specific to this item+      formIdList :: [Integer]     } deriving (Eq, Ord) +-- | The zero ID, i.e. the first ID that is usable+--+zeroId :: String -> FormId+zeroId p = FormId+    { formPrefix = p+    , formIdList = [0]+    }++mapId :: ([Integer] -> [Integer]) -> FormId -> FormId+mapId f (FormId p is) = FormId p $ f is+ instance Show FormId where-    show (FormId p x) = p ++ "-f" ++ show x+    show (FormId p xs) =+        p ++ "-fval[" ++ (intercalate "." $ reverse $ map show xs) ++ "]" +formId :: FormId -> Integer+formId = head . formIdList+ -- | A range of ID's to specify a group of forms -- data FormRange = FormRange FormId FormId@@ -53,14 +80,18 @@ -- | Increment a form ID -- incrementFormId :: FormId -> FormId-incrementFormId (FormId p x) = FormId p $ x + 1+incrementFormId (FormId p (x:xs)) = FormId p $ (x + 1):xs+incrementFormId (FormId _ []) = error "Bad FormId list" +unitRange :: FormId -> FormRange+unitRange i = FormRange i $ incrementFormId i+ -- | Check if a 'FormId' is contained in a 'FormRange' -- isInRange :: FormId     -- ^ Id to check for           -> FormRange  -- ^ Range           -> Bool       -- ^ If the range contains the id-isInRange (FormId _ a) (FormRange b c) = a >= formId b && a < formId c+isInRange a (FormRange b c) = formId a >= formId b && formId a < formId c  -- | Check if a 'FormRange' is contained in another 'FormRange' --
src/Text/Digestive/Transform.hs view
@@ -3,6 +3,7 @@ module Text.Digestive.Transform     ( Transformer (..)     , transform+    , transformFormlet     , transformEither     , transformEitherM     , transformRead@@ -51,6 +52,16 @@                 Left e -> (v1, Error $ map ((,) range) e)                 -- All fine                 Right y -> (v1, Ok y)++-- | Apply a transformer to a formlet+--+transformFormlet :: Monad m+                 => (b -> a)             -- ^ Needed to produce defaults+                 -> Formlet m i e v a    -- ^ Formlet to transform+                 -> Transformer m e a b  -- ^ Transformer+                 -> Formlet m i e v b    -- ^ Resulting formlet+transformFormlet f formlet transformer def =+    formlet (fmap f def) `transform` transformer  -- | Build a transformer from a simple function that returns an 'Either' result. --
src/Text/Digestive/Types.hs view
@@ -9,8 +9,12 @@     , getFormId     , getFormRange     , getFormInput+    , getFormInput'     , isFormInput     , Form (..)+    , Formlet+    , bracketState+    , incState     , view     , (++>)     , (<++)@@ -78,8 +82,12 @@ -- | Utility function: Get the current input -- getFormInput :: Monad m => FormState m i (Maybe i)-getFormInput = do-    id' <- getFormId+getFormInput = getFormId >>= getFormInput'++-- | Gets the input of an arbitrary FormId.+--+getFormInput' :: Monad m => FormId -> FormState m i (Maybe i)+getFormInput' id' = do     env <- ask     case env of Environment f -> lift $ lift $ f id'                 NoEnvironment -> return Nothing@@ -95,6 +103,10 @@ -- newtype Form m i e v a = Form {unForm :: FormState m i (View e v, Result e a)} +-- | A function for generating forms with an optional default value.+--+type Formlet m i e v a = Maybe a -> Form m i e v a+ instance Monad m => Functor (Form m i e v) where     fmap f form = Form $ do         (view', result) <- unForm form@@ -104,17 +116,25 @@     pure x = Form $ return (mempty, return x)     f1 <*> f2 = Form $ do         -- Assuming f1 already has a valid ID-        FormRange startF1 _ <- get-        (v1, r1) <- unForm f1-        FormRange _ endF1 <- get+        ((v1,r1), (v2,r2)) <- bracketState $ do+            res1 <- unForm f1+            incState+            res2 <- unForm f2+            return (res1, res2)+        return (v1 `mappend` v2, r1 <*> r2) -        -- Set a new, empty range-        put $ FormRange endF1 $ incrementFormId endF1-        (v2, r2) <- unForm f2-        FormRange _ endF2 <- get+bracketState :: Monad m => FormState m i a -> FormState m i a+bracketState k = do+    FormRange startF1 _ <- get+    res <- k+    FormRange _ endF2 <- get+    put $ FormRange startF1 endF2+    return res -        put $ FormRange startF1 endF2-        return (v1 `mappend` v2, r1 <*> r2)+incState :: Monad m => FormState m i ()+incState = do+        FormRange _ endF1 <- get+        put $ unitRange endF1  -- | Insert a view into the functor --@@ -168,9 +188,7 @@         -> Environment m i           -- ^ Input environment         -> m (View e v, Result e a)  -- ^ Result runForm form id' env = evalStateT (runReaderT (unForm form) env) $-    FormRange f0 $ incrementFormId f0-  where-    f0 = FormId id' 0+    unitRange $ zeroId id'  -- | Evaluate a form to it's view if it fails --