Skip to content

HTTP server library inspired by axum; best-in-class expressiveness & backend-agnostic.

Notifications You must be signed in to change notification settings

cakekindel/purescript-axon

Repository files navigation

axon

WIP

HTTP server library inspired by axum, allowing best-in-class expressive routing.

The main difference between this server library compared to others (eg. the wonderful httpurple) is the philosophy around routing.

The core abstraction is a Handler; any function with the type [...Request parts] -> m Response.

This allows each REST action to correspond to a single function, which declares its requirements in its type signature. This allows for a highly refactorable & composable application, as opposed to a hierarchical routing approach like routing-duplex.

For example, an endpoint GET /persons/:id/address would be modeled as:

getPersonAddress :: Get -> Path ("persons" / Int / "address") Int -> Aff Response
getPersonAddress _ (Path id) = ...

POST /persons accepting a json body:

type Person = { firstName :: String, lastName :: String, age :: Maybe Int }

postPerson :: Post -> Path "persons" Unit -> ContentType Json -> Json Person -> Aff Response
postPerson _ _ _ person = ...

Then these can be rolled up into a /persons resource with Handler.or:

persons :: Handler Aff Response
persons = getPerson `Handler.or` postPerson `Handler.or` deletePerson `Handler.or` getPersonAddress ...

Then run with:

Axon.serveNode {port: 10000, hostname: "0.0.0.0"} persons

Example

This example implements this REST interface in 36LoC:

  • GET /cheeses - Lists all cheeses (strings) known to server
  • POST /cheeses - Add a cheese to the cheese list
  • DELETE /cheeses/:cheese - Remove a cheese from the cheese list
module Main where

import Prelude

import Axon as Axon
import Axon.Request.Handler as Handler
import Axon.Request.Parts.Class (Delete, Get, Path(..), Post)
import Axon.Request.Parts.Path (type (/))
import Axon.Response (Response)
import Axon.Response.Construct (Json(..), toResponse)
import Axon.Response.Status as Status
import Data.Filterable (filter)
import Data.Foldable (elem)
import Data.Tuple.Nested ((/\))
import Effect (Effect)
import Effect.Aff (Aff, launchAff_, joinFiber)
import Effect.Aff as Aff
import Effect.Class (liftEffect)
import Effect.Ref (Ref)
import Effect.Ref as Ref

main :: Effect Unit
main = launchAff_ do
  cheeses :: Ref (Array String) <- liftEffect $ Ref.new
    [ "cheddar", "swiss", "gouda" ]

  let
    getCheeses :: Get -> Path "cheeses" _ -> Aff Response
    getCheeses _ _ = liftEffect do
      cheeses' <- Ref.read cheeses
      toResponse $ Status.ok /\ Json cheeses'

    deleteCheese :: Delete -> Path ("cheeses" / String) _ -> Aff Response
    deleteCheese _ (Path id) = liftEffect do
      cheeses' <- Ref.read cheeses
      if not $ elem id cheeses' then
        toResponse Status.notFound
      else
        Ref.modify_ (filter (_ /= id)) cheeses
        *> toResponse Status.accepted

    postCheese :: Post -> Path "cheeses" _ -> String -> Aff Response
    postCheese _ _ cheese =
      let
        tryInsert as
          | elem cheese as = { state: as, value: false }
          | otherwise = { state: as <> [ cheese ], value: true }
      in
        liftEffect
          $ Ref.modify' tryInsert cheeses
          >>= if _ then toResponse Status.accepted else toResponse Status.conflict

  handle <-
    Axon.serveBun
      { port: 8080, hostname: "localhost" }
      (getCheeses `Handler.or` postCheese `Handler.or` deleteCheese)

  joinFiber handle.join

Request Handlers

Request handler functions have any number of parameters that are RequestParts and return an Aff Response (or any MonadAff).

RequestParts

  • Request
    • Always succeeds; provides the entire request
  • Combinators
    • Unit
      • Always succeeds
    • a /\ b
      • Tuple of a and b, where a and b are RequestParts.
    • Maybe a
      • a must be RequestParts. If a can't be extracted, the handler will still succeed and this will be Nothing. If a was extracted, it's wrapped in Just.
    • Either a b
      • a and b must be RequestParts. Succeeds if either a or b succeeds (preferring a). Fails if both fail.
  • Body
    • String
      • succeeds when request has a non-empty body that is valid UTF-8
    • Json a
      • succeeds when request has a String body (see above) that can be parsed into a using DecodeJson.
    • Buffer
      • succeeds when request has a nonempty body.
    • Stream
      • succeeds when request has a nonempty body.
  • Headers
    • Header a
      • a must be TypedHeader from Axon.Header.Typed. Allows statically (ex. ContentType Type.MIME.Json) or dynamically (ex. ContentType String) matching request headers.
    • HeaderMap
      • All headers provided in the request
  • Path
    • Path a c
      • Statically match the path of the request, and extract parameters. See Axon.Request.Parts.Path. (TODO: this feels too magical, maybe follow axum's prior art of baking paths into the router declaration?)
  • Method - Get - Post - Put - Patch - Delete - Options - Connect - Trace

Similarly to the structural extraction of request parts; handlers can use Axon.Response.Construct.ToResponse for easily constructing responses.

ToResponse

  • Combinators
    • Status /\ a
      • Special case to make sure any Status in a tuple will take priority over any default statuses within. TODO: This case (overlapping with a /\ b requires the class to be "sealed" in an instance chain. Want a clean way around this so consumers can implement ToResponse.)
    • a /\ b
      • Merges toResponse a and toResponse b, using b on conflicts
  • Status
    • Axon.Response.Status.Status
  • Body
    • Axon.Response.Body.Body
    • String
    • Node.Buffer.Buffer
    • Node.Stream.Readable a (for all a)
    • Axon.Response.Construct.Json a
      • a must be EncodeJson. This will set the body to a stringified, and set Content-Type to application/json.
  • Headers
    • ToResponse is implemented for all implementors of TypedHeader
    • TODO: Map String String

About

HTTP server library inspired by axum; best-in-class expressiveness & backend-agnostic.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published