Bringing Sanity to the SPA

A Short "Why & How"

on using CodeGen via purescript-bridge

Goals

  • Stay sane
  • write less code

Goals

[      ] always-correct interfaces for queries to build on.

[      ] ensure handling all responses from the server.

 

Forms

Forms

-- our database uses this representation
data Curator = Curator 
  { email      :: EmailAddress
  , message    :: Maybe Text
  , inviter    :: UserId
  , token      :: Text
  , invited_on :: UTCTime }

Database expectation

Forms

-- our database uses this representation
data Curator = Curator 
  { email      :: EmailAddress
  , message    :: Maybe Text
  , inviter    :: UserId
  , token      :: Text
  , invited_on :: UTCTime }
-- our database uses this representation
data CuratorForm = CuratorForm
  { email      :: EmailAddress
  , message    :: Maybe Text
  } deriving (Generic, Typeable, Show)

Subset of your expectation

Forms

-- our database uses this representation
data Curator = Curator 
  { email      :: EmailAddress
  , message    :: Maybe Text
  , inviter    :: UserId
  , token      :: Text
  , invited_on :: UTCTime }
-- our database uses this representation
data CuratorForm = CuratorForm
  { email      :: EmailAddress
  , message    :: Maybe Text 
  } deriving (Generic, Typeable, Show)

Subset of your expectation

Gen This!

Don't

initialState = { form: curatorForm }

curatorForm = CuratorForm { email: "", message: Nothing }

render state = do
  -- ...
  div_ $ Form.renderForm state.form NewCurator do
    Form.textField "email"   "Email"  _email   (Form.nonBlank <=< Form.emailValidator)
    Form.textField "message" "A Note" _message Form.optional

A Small Example

initialState = { form: curatorForm }

curatorForm = CuratorForm { email: "", message: Nothing }

render state = do
  -- ...
  div_ $ Form.renderForm state.form NewCurator do
    Form.textField "email"   "Email"  _email   (Form.nonBlank <=< Form.emailValidator)
    Form.textField "message" "A Note" _message Form.optional

A Small Example

initialState = { form: curatorForm }

curatorForm = CuratorForm { email: "", message: Nothing }

render state = do
  -- ...
  div_ $ Form.renderForm state.form NewCurator do
    Form.textField "email"   "Email"  _email   (Form.nonBlank <=< Form.emailValidator)
    Form.textField "message" "A Note" _message Form.optional
  • can't fail on the frontend

  • compiler tells us what is valid

  • changes to backend alter frontend interfaces (as they should!)

A Small Example

[ ✓ ] always-correct interfaces for queries to build on.

[      ] ensure handling all responses from the server.


Serializing the Response

What could happen?

  • Success!
  • Already sent
  • Failure :C

Serializing the Response

What could happen?

  • Success!
  • Already sent
  • Failure :C
data CreateResponse
  = CreateSuccess Int64
  | CreateFailure Text
  | NotUnique
  deriving (Generic, Typeable, Show)

Gen This!

-- the CuratorForm is the 'form' field of our state.
response <- post (apiUrl <> "/admin/curators") (encodeJson state.form)
let Tuple typ msg = handleCreateResponse "admin.curator" response
return $ flashMessage typ msg

Curator Creation

  • encode the (guaranteed correct) form
  • post it
  • use generic response handler
  • flash message based on this
-- the CuratorForm is the 'form' field of our state.
response <- post (apiUrl <> "/admin/curators") (encodeJson state.form)
let Tuple typ msg = handleCreateResponse "admin.curator" response
return $ flashMessage typ msg

Curator Creation

handleCreateResponse :: TranslationIndex -> Json -> Tuple Status String
handleCreateResponse idx res =
  case decodeJson res.response of
    Left e -> Tuple Failure e
    Right cr ->  do
      let message = translate idx cr
      case cr of
        CreateSuccess _ -> Tuple Success message
        NotUnique       -> Tuple Warning message
        CreateFailure t -> Tuple Failure message
translate :: IsTranslatable a => TranslationIndex -> a -> String
handleCreateResponse :: TranslationIndex -> Json -> Tuple Status String
handleCreateResponse idx res =
  case decodeJson res.response of
    Left e -> Tuple Failure e
    Right cr ->  do
      let message = translate idx cr
      case cr of
        CreateSuccess _ -> Tuple Success message
        NotUnique       -> Tuple Warning message
        CreateFailure t -> Tuple Failure message

[ ✓ ] always-correct interfaces for queries to build on.

[ ✓ ] ensure handling all responses from the server.

 

[ ✓ ] always-correct interfaces for queries to build on.

[ ✓ ] ensure handling all responses from the server.

 

!!🎉🎉💯🎉🎉!!

How Tho?!

myTypes :: [SumType 'Haskell]
myTypes = [
    mkSumType (Proxy :: Proxy CuratorForm)
  , mkSumType (Proxy :: Proxy CreateResponse)
  -- ...
  ]

The Bridge

"register" those types with purescript-bridge

myTypes :: [SumType 'Haskell]
myTypes = [
    mkSumType (Proxy :: Proxy CuratorForm)
  , mkSumType (Proxy :: Proxy CreateResponse)
  -- ...
  ]

The Bridge

"register" those types with purescript-bridge

 

backend types  =>  frontend types

not a perfect conversion

give the bridge some direction

import Language.PureScript.Bridge.PSTypes (psInt)

-- delegate to a primitive
int64Bridge :: BridgePart
int64Bridge = typeName ^== "Int64" >> return psInt

Delegating

"Purescript, You already have this and it's called something else"

psDateTime :: TypeInfo 'PureScript
psDateTime = TypeInfo {
    _typePackage = ""
  , _typeModule = "Types"
  , _typeName = "DateStamp"
  , _typeParameters = []
  }

utcTimeBridge :: BridgePart
utcTimeBridge = typeName ^== "UTCTime" >> return psDateTime

Super-Delegating

"Haskell, just lemme do it"

psDateTime :: TypeInfo 'PureScript
psDateTime = TypeInfo {
    _typePackage = ""
  , _typeModule = "Types"
  , _typeName = "DateStamp"
  , _typeParameters = []
  }

utcTimeBridge :: BridgePart
utcTimeBridge = typeName ^== "UTCTime" >> return psDateTime

Super-Delegating

In the module "Types.purs"

psDateTime :: TypeInfo 'PureScript
psDateTime = TypeInfo {
    _typePackage = ""
  , _typeModule = "Types"
  , _typeName = "DateStamp"
  , _typeParameters = []
  }

utcTimeBridge :: BridgePart
utcTimeBridge = typeName ^== "UTCTime" >> return psDateTime

Super-Delegating

In the module "Types.purs"
you'll find a type "DateStamp"

psDateTime :: TypeInfo 'PureScript
psDateTime = TypeInfo {
    _typePackage = ""
  , _typeModule = "Types"
  , _typeName = "DateStamp"
  , _typeParameters = []
  }

utcTimeBridge :: BridgePart
utcTimeBridge = typeName ^== "UTCTime" >> return psDateTime

Super-Delegating

In the module "Types.purs"
you'll find a type "DateStamp"
It requires no installed packages

 

psDateTime :: TypeInfo 'PureScript
psDateTime = TypeInfo {
    _typePackage = ""
  , _typeModule = "Types"
  , _typeName = "DateStamp"
  , _typeParameters = []
  }

utcTimeBridge :: BridgePart
utcTimeBridge = typeName ^== "UTCTime" >> return psDateTime

Super-Delegating

In the module "Types.purs"
you'll find a type "DateStamp"
It requires no installed packages

Use it if we see UTCTime

main :: IO ()
main = writePSTypes "frontend/src/" (buildBridge mainBridge) myTypes
  where
    mainBridge = defaultBridge <|> int64Bridge <|> utcTimeBridge

Composing a bridge

  • specify where to put the codegen
  • which pieces are needed to translate
  • hook this IO function into build-chain

Thanks!