diff --git a/examples/SafeForm.purs b/examples/SafeForm.purs new file mode 100644 index 0000000..20170ec --- /dev/null +++ b/examples/SafeForm.purs @@ -0,0 +1,107 @@ +module Examples.SafeForm where + +import Control.IxMonad ((:*>)) +import Control.Monad.Eff (Eff) +import Control.Monad.Eff.Console (CONSOLE) +import Control.Monad.Except (ExceptT) +import Data.Foldable (traverse_) +import Data.Maybe (maybe) +import Data.MediaType.Common (textHTML) +import Data.Monoid (mempty) +import Hyper.Node.Server (defaultOptionsWithLogging, runServer) +import Hyper.Response (closeHeaders, contentType, respond, writeStatus) +import Hyper.Routing (type (:/), type (:<|>), type (:>), Capture, (:<|>)) +import Hyper.Routing.ContentType.HTML (class EncodeHTML, HTML, linkTo) +import Hyper.Routing.Form (type (:<>), InputHidden, InputNumber, InputText, toForms) +import Hyper.Routing.Links (linksTo) +import Hyper.Routing.Method (Get, Post) +import Hyper.Routing.Router (RoutingError, router) +import Node.Buffer (BUFFER) +import Node.HTTP (HTTP) +import Text.Smolder.HTML (h1, p, table, tbody, td, th, thead, tr) +import Text.Smolder.Markup (text) +import Type.Proxy (Proxy(..)) +import Prelude hiding (div) + +type Site = + Get HTML Persons + :<|> "new" :/ Get HTML NewPerson + :<|> Post HTML PersonSaved + :<|> Capture "id" Int :> Get HTML EditPerson + +newtype Person = Person { id :: Int, name ∷ String, age ∷ Int } + +data Persons = Persons (Array Person) +data NewPerson = NewPerson +data EditPerson = EditPerson Person +data PersonSaved = PersonSaved + +type PersonForm = + InputHidden "id" Int + :<> InputText "name" + :<> InputNumber "age" + +instance encodeHTMLPersons :: EncodeHTML Persons where + encodeHTML (Persons ps) = + table do + thead do + tr do + th (text "Name") + th (text "Age") + th (text "Actions") + tbody (traverse_ encodePerson ps) + where + encodePerson (Person person) = + case linksTo site of + _ :<|> _ :<|> getPerson' → + tr do + td (text person.name) + td (text (show person.age)) + td (linkTo (getPerson' person.id) (text "Edit")) + +instance encodeHTMLNewPerson :: EncodeHTML NewPerson where + encodeHTML _ = + case toForms site of + _ :<|> _ :<|> savePersonForm :<|> _ → do + p (text "New Person") + savePersonForm + +instance encodeHTMLPerson :: EncodeHTML EditPerson where + encodeHTML (EditPerson (Person person)) = do + h1 (text "Edit Person") + p (text "TODO") + +instance encodeHTMLPersonSaved :: EncodeHTML PersonSaved where + encodeHTML _ = mempty + +allPersons ∷ ∀ m. Monad m ⇒ ExceptT RoutingError m Persons +allPersons = + [Person { id: 1, name: "Alice", age: 41 }] + # Persons + # pure + +newPerson ∷ ∀ m. Monad m ⇒ ExceptT RoutingError m NewPerson +newPerson = pure NewPerson + +editPerson ∷ ∀ m. Monad m ⇒ Int → ExceptT RoutingError m EditPerson +editPerson i = + Person { id: 0, name: "John", age: 41 } + # EditPerson + # pure + +savePerson ∷ ∀ m. Monad m ⇒ ExceptT RoutingError m PersonSaved +savePerson = pure PersonSaved + +site :: Proxy Site +site = Proxy + +main :: forall e. Eff (http :: HTTP, console :: CONSOLE, buffer :: BUFFER | e) Unit +main = + let onRoutingError status msg = + writeStatus status + :*> contentType textHTML + :*> closeHeaders + :*> respond (maybe "" id msg) + handlers = allPersons :<|> newPerson :<|> savePerson :<|> editPerson + appRouter = router site handlers onRoutingError + in runServer defaultOptionsWithLogging {} appRouter diff --git a/src/Hyper/Form.purs b/src/Hyper/Form.purs index 453a76c..4f97779 100644 --- a/src/Hyper/Form.purs +++ b/src/Hyper/Form.purs @@ -73,3 +73,9 @@ parseForm = ipure (Form <$> splitPairs conn.request.body) Just mediaType -> ipure (throwError (error ("Cannot parse media of type: " <> show mediaType))) + +class ToForm a where + toForm ∷ a → Form + +class FromForm a where + fromForm ∷ Form → Either String Form diff --git a/src/Hyper/Routing.purs b/src/Hyper/Routing.purs index 60b04c1..03c32ff 100644 --- a/src/Hyper/Routing.purs +++ b/src/Hyper/Routing.purs @@ -4,6 +4,7 @@ module Hyper.Routing ( Lit , Capture , CaptureAll + , ReqBody , Handler , Raw , Sub @@ -30,6 +31,12 @@ data Capture (v :: Symbol) t -- | is a `Symbol` that describes data CaptureAll (v :: Symbol) t +-- | Captures and parses the request body in any of the specified content +-- | types. The `ct` type parmeter can be a single content type, or an `AltE` +-- | list of content types to allow. There has to be implementations of +-- | `FromFormData` for `t` and all specified content types. +data ReqBody ct t + -- | A type-level description of the handler function, terminating a chain of -- | path literals, captures, and other endpoint type constructs. The `m` symbol -- | is the HTTP method that is handled. `ct` is the content type. diff --git a/src/Hyper/Routing/ContentType/HTML.purs b/src/Hyper/Routing/ContentType/HTML.purs index d095966..a7bfcfe 100644 --- a/src/Hyper/Routing/ContentType/HTML.purs +++ b/src/Hyper/Routing/ContentType/HTML.purs @@ -17,6 +17,9 @@ import Text.Smolder.Renderer.String (render) data HTML +-- TODO: Enforce that URI comes from a GET-able resource, +-- perhaps by wrapping the URI type and adding some phantom +-- type parameter for the HTTP method. linkTo :: URI -> Markup Unit -> Markup Unit linkTo uri = a ! href (printURI uri) diff --git a/src/Hyper/Routing/Form.purs b/src/Hyper/Routing/Form.purs new file mode 100644 index 0000000..21b1429 --- /dev/null +++ b/src/Hyper/Routing/Form.purs @@ -0,0 +1,107 @@ +module Hyper.Routing.Form where + +import Prelude +import Data.String as String +import Data.Maybe (Maybe, maybe) +import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol) +import Hyper.Routing (type (:<|>), type (:>), Capture, CaptureAll, Handler, Lit, (:<|>)) +import Hyper.Routing.PathPiece (class ToPathPiece, toPathPiece) +import Text.Smolder.HTML (form, input) +import Text.Smolder.HTML.Attributes (action, method, name, type', value) +import Text.Smolder.Markup (MarkupM, (!)) +import Type.Proxy (Proxy(..)) + +data FAppend a b = FAppend a b + +infixl 4 type FAppend as :<> +infixl 4 FAppend as :<> + +data InputText (name ∷ Symbol) = InputText String +data InputHidden (name ∷ Symbol) a = InputHidden a +data InputNumber (name ∷ Symbol) = InputNumber Int + +class ToFormFields f o | f → o where + toFields ∷ Proxy f → o + +instance toFormFieldsInputText ∷ IsSymbol name + ⇒ ToFormFields (InputText name) (Maybe String → MarkupM Unit Unit) where + toFields _ s = maybe field (\x → field ! value x) s + where + n = reflectSymbol (SProxy ∷ SProxy name) + field = input ! name n ! type' "text" + +instance toFormFieldsInputNumber ∷ IsSymbol name + ⇒ ToFormFields (InputNumber name) (Maybe Int → MarkupM Unit Unit) where + toFields _ x = maybe field (\x' → field ! value (show x')) x + where + n = reflectSymbol (SProxy ∷ SProxy name) + field = input ! name n ! type' "number" + +instance toFormFieldsInputHidden ∷ (IsSymbol name, Show a) + ⇒ ToFormFields (InputHidden name a) (a → MarkupM Unit Unit) where + toFields _ x = input ! name n ! type' "hidden" ! value (show x) + where + n = reflectSymbol (SProxy ∷ SProxy name) + +instance toFormFieldsFConcat ∷ (ToFormFields f1 m1, ToFormFields f2 m2) + ⇒ ToFormFields (f1 :<> f2) (m1 :<> m2) where + toFields _ = toFields p1 :<> toFields p2 + where + p1 = Proxy ∷ Proxy f1 + p2 = Proxy ∷ Proxy f2 + +data FormContext + = FormContext (Array String) + +class ToForm e r | e -> r where + toForm :: Proxy e -> FormContext -> r + +instance toFormAltE :: (ToForm e1 r1, ToForm e2 r2) + => ToForm (e1 :<|> e2) (r1 :<|> r2) where + toForm _ ctx = + toForm p1 ctx :<|> toForm p2 ctx + where + p1 = Proxy ∷ Proxy e1 + p2 = Proxy ∷ Proxy e2 + +instance toFormLit :: (ToForm sub r, IsSymbol lit) + => ToForm (Lit lit :> sub) r where + toForm _ (FormContext segments) = + toForm (Proxy ∷ Proxy sub) ctx + where + ctx = FormContext (segments <> [segment]) + segment = reflectSymbol (SProxy ∷ SProxy lit) + +instance toFormCapture :: (ToForm sub r, IsSymbol c, ToPathPiece t) + => ToForm (Capture c t :> sub) (t → r) where + toForm _ (FormContext segments) x = + toForm (Proxy ∷ Proxy sub) ctx + where + ctx = FormContext (segments <> [toPathPiece x]) + +instance toFormCaptureAll :: (ToForm sub r, IsSymbol c, ToPathPiece t) + => ToForm (CaptureAll c t :> sub) (Array t → r) where + toForm _ (FormContext segments) x = + toForm (Proxy ∷ Proxy sub) ctx + where + ctx = FormContext (segments <> (map toPathPiece x)) + +-- TODO: Consider supporting GET with forms and query params. +instance toFormHandlerGet :: ToForm + (Handler "GET" ct b) + Unit where + toForm _ (FormContext segments) = unit + +instance toFormHandlerPost :: ToForm + (Handler "POST" ct b) + (MarkupM Unit Unit) where + toForm _ (FormContext segments) = + form ! method "POST" ! action path $ pure unit + where + path = "/" <> String.joinWith "/" segments + +toForms + ∷ ∀ e m. ToForm e m + ⇒ Proxy e + → m +toForms p = toForm p (FormContext []) diff --git a/src/Hyper/Routing/Links.purs b/src/Hyper/Routing/Links.purs index 58783ab..14b7cd1 100644 --- a/src/Hyper/Routing/Links.purs +++ b/src/Hyper/Routing/Links.purs @@ -67,7 +67,7 @@ instance hasLinksCaptureAll :: (HasLinks sub subMk, IsSymbol c, ToPathPiece t) toLinks _ l = toLinks (Proxy :: Proxy sub) <<< append l <<< Link <<< map toPathPiece -instance hasLinksHandler :: HasLinks (Handler m ct b) URI where +instance hasLinksHandlerGet :: HasLinks (Handler m ct b) URI where toLinks _ = linkToURI instance hasLinksRaw :: HasLinks (Raw m) URI where