devforms 0.1.0.1 → 0.1.0.3
raw patch · 5 files changed
+84/−2 lines, 5 filesPVP: minor bump suggested
API additions: PVP suggests at least a minor version bump
API changes (from Hackage documentation)
+ DevForms: type FormBuilder = Writer (Endo Form)
+ DevForms: type QuestionBuilder = Writer (Endo QuestionOptions)
+ DevForms: type ServerBuilder = Writer (Endo Server)
Files
- devforms.cabal +1/−1
- src/DevForms.hs +77/−1
- src/Form.hs +2/−0
- src/Question.hs +2/−0
- src/Server.hs +2/−0
devforms.cabal view
@@ -1,6 +1,6 @@ cabal-version: 3.0 name: devforms-version: 0.1.0.1+version: 0.1.0.3 synopsis: A builder DSL for HTML survey forms with built-in server and storage description: devforms is a Haskell library for building HTML survey forms using a
src/DevForms.hs view
@@ -1,13 +1,45 @@ {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} -module DevForms (devFormServer, form, questionCheckbox, questionLikert, questionChoice, questionDate, questionTime, questionInteger, setLowerBoundInclusive, setUpperBoundInclusive) where+{- |+Module : DevForms+Description : A builder DSL for creating HTML survey forms +DevForms lets you define multi-page survey forms using a concise monadic+builder DSL. Forms are served via a built-in web server (Scotty), and+submissions are stored as JSONL files.++=== Example++@+main :: IO ()+main = devFormServer 9000 $ do+ form "Animal Survey" "animals" $ do+ questionLikert "I enjoy seeing animals"+ questionChoice "Favourite animal"+ ["Alpaca", "Bumblebee", "Camel", "Duck"]+ questionDate "When would you like to visit the zoo?"+ questionInteger "How many tickets?" $ do+ setLowerBoundInclusive 1+ setUpperBoundInclusive 10+@+-}+module DevForms (ServerBuilder, FormBuilder, QuestionBuilder, devFormServer, form, questionCheckbox, questionLikert, questionChoice, questionDate, questionTime, questionInteger, setLowerBoundInclusive, setUpperBoundInclusive) where+ import Control.Monad.Writer import Form (Form (..), FormBuilder) import Question (Question (..), QuestionBuilder, QuestionOptions (..)) import Server (Server (..), ServerBuilder, runServer) +{- | Start the devforms web server on the given port.++This is the top-level entry point for a devforms application. The second+argument is a 'ServerBuilder' block in which you define one or more forms+using 'form'. The server provides:++* Individual form pages with client-side validation+* A submission endpoint that stores answers in @answers-\<formId\>.jsonl@+-} devFormServer :: Int -> ServerBuilder () -> IO () devFormServer = runServer @@ -15,6 +47,17 @@ -- let forms = appEndo (execWriter serverBuilder) $ Server{forms = []} -- print $ forms +{- | Define a survey form.++The first argument is the human-readable title displayed at the top of the+form page. The second argument is a form identifier used for:++* URL routing — the form is served at @/form/\<formId\>@+* Persistent storage — submissions are appended to @answers-\<formId\>.jsonl@++The third argument is a 'FormBuilder' block where you add questions using+the @question*@ functions.+-} form :: Text -> Text -> FormBuilder () -> ServerBuilder () form title formId formBuilder = do let f = appEndo (execWriter formBuilder) $ Form{title = title, formId = formId, questions = []}@@ -24,29 +67,62 @@ addQuestion question = tell $ Endo $ \f@Form{questions} -> f{questions = questions <> [question]} +{- | Add a yes\/no checkbox question. Renders as a single checkbox that the+respondent can tick or leave unticked.+-} questionCheckbox :: Text -> FormBuilder () questionCheckbox = addQuestion . QuestionCheckbox +{- | Add a Likert-scale question. Renders as a 5-point agreement scale+(Strongly disagree … Strongly agree) plus a \"Cannot say\" option.+-} questionLikert :: Text -> FormBuilder () questionLikert = addQuestion . QuestionLikert +{- | Add a multiple-choice question. Renders as a group of radio buttons — the+respondent must select exactly one of the provided options.++The first argument is the question label; the second is the list of choices.+-} questionChoice :: Text -> [Text] -> FormBuilder () questionChoice title qOptions = addQuestion $ QuestionChoice title qOptions +{- | Add a date-picker question. Renders as an HTML date input and stores the+answer in @YYYY-MM-DD@ format.+-} questionDate :: Text -> FormBuilder () questionDate = addQuestion . QuestionDate +{- | Add a time-picker question. Renders as an HTML time input and stores the+answer in @HH:MM@ format.+-} questionTime :: Text -> FormBuilder () questionTime = addQuestion . QuestionTime +{- | Add an integer input question. The second argument is a 'QuestionBuilder'+block where you can optionally configure bounds using+'setLowerBoundInclusive' and 'setUpperBoundInclusive'. Bounds are enforced+both client-side (via HTML attributes) and server-side on submission.++=== Example++@+questionInteger "How many pets do you have?" $ do+ setLowerBoundInclusive 0+ setUpperBoundInclusive 50+@+-} questionInteger :: Text -> QuestionBuilder () -> FormBuilder () questionInteger title questionBuilder = do let questionOptions = appEndo (execWriter questionBuilder) $ QuestionOptions{lowerBoundInclusive = Nothing, upperBoundInclusive = Nothing} addQuestion $ QuestionInteger title questionOptions +-- | Set the minimum allowed value (inclusive) for a 'questionInteger'. setLowerBoundInclusive :: Integer -> QuestionBuilder () setLowerBoundInclusive bound = tell $ Endo $ \questionOptions -> questionOptions{lowerBoundInclusive = Just bound}++-- | Set the maximum allowed value (inclusive) for a 'questionInteger'. setUpperBoundInclusive :: Integer -> QuestionBuilder () setUpperBoundInclusive bound = tell $ Endo $ \questionOptions -> questionOptions{upperBoundInclusive = Just bound}
src/Form.hs view
@@ -6,6 +6,8 @@ import Question (Question, renderQuestion) data Form = Form {title :: Text, formId :: Text, questions :: [Question]} deriving (Show)++-- | A builder monad for adding questions to a form. Use the @question*@ functions to add questions. type FormBuilder = Writer (Endo Form) renderForm :: Form -> Html ()
src/Question.hs view
@@ -11,6 +11,8 @@ import Relude.Extra.Map as Map data Question = QuestionCheckbox Text | QuestionLikert Text | QuestionChoice Text [Text] | QuestionDate Text | QuestionInteger Text QuestionOptions | QuestionTime Text deriving (Show)++-- | A builder monad for configuring question options (e.g. bounds for integer questions). type QuestionBuilder = Writer (Endo QuestionOptions) data QuestionOptions = QuestionOptions
src/Server.hs view
@@ -19,6 +19,8 @@ import Web.Scotty qualified as Scotty data Server = Server {forms :: [Form]} deriving (Show)++-- | A builder monad for configuring the devforms server. Use 'form' to add forms. type ServerBuilder = Writer (Endo Server) staticFiles :: Map FilePath ByteString