Skip to content

Commit

Permalink
Refactor and update for PS 0.14 (#18)
Browse files Browse the repository at this point in the history
* Replaced Simple.Json with Argonaut, removed Apiary

* Removed foreign-generic

* Purescript version up to v0.14.0 + refactor, replaced event with halogen-subscriptions

* Replaced wire-react-router with web-router

* Removed AppInstance + refactor
  • Loading branch information
jonasbuntinx authored Apr 5, 2021
1 parent 796327f commit 596cbaa
Show file tree
Hide file tree
Showing 45 changed files with 1,330 additions and 1,528 deletions.
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,18 +52,14 @@ An implementation of React hooks on top of purescript-react-basic.

A Halogen-inspired interface for React.

#### [Wire React Router](https://github.com/robertdp/purescript-wire-react-router)
#### [Web Router](https://github.com/robertdp/purescript-web-router)

A basic pushstate router for React, with support for asynchronous routing logic.
A basic web router with support for asynchronous routing logic.

#### [Routing Duplex](https://github.com/natefaubion/purescript-routing-duplex)

Unified parsing and printing for routes in PureScript.

#### [Apiary](https://github.com/robertdp/purescript-apiary)

For the creation of type-level specs that can be queried against automatically.

## Recognition

I was inspired by [Thomas Honeyman](https://github.com/thomashoneyman)'s [implementation](https://github.com/thomashoneyman/purescript-halogen-realworld) of the Real World spec using [Halogen](https://github.com/slamdata/purescript-halogen).
4 changes: 2 additions & 2 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@
<title>Conduit</title>
<link
href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
rel="preload"
rel="stylesheet"
media="print"
onload="this.media='all'; this.onload=null;"
/>
<link
href="//fonts.googleapis.com/css?family=Titillium+Web:700|Source+Serif+Pro:400,700|Merriweather+Sans:400,700|Source+Sans+Pro:400,300,600,700,300italic,400italic,600italic,700italic"
rel="preload"
rel="stylesheet"
media="print"
onload="this.media='all'; this.onload=null;"
/>
Expand Down
13 changes: 7 additions & 6 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "purescript-react-realworld",
"version": "1.0.0",
"version": "3.0.0",
"description": "A real-world application demonstrating PureScript and React",
"keywords": [
"Purescript",
Expand All @@ -9,7 +9,8 @@
],
"contributors": [
{
"name": "Jonas Buntinx"
"name": "Jonas Buntinx",
"url": "https://github.com/jonasbuntinx"
},
{
"name": "Robert Porter",
Expand All @@ -30,17 +31,17 @@
"test": "spago test --no-install"
},
"devDependencies": {
"parcel": "^1.12.4",
"purescript": "^0.13.8",
"parcel": "1.12.3",
"purescript": "^0.14.0",
"purescript-psa": "^0.8.2",
"purty": "^6.3.1",
"purty": "^7.0.0",
"spago": "^0.19.1",
"zephyr": "https://github.com/jonasbuntinx/zephyr.git"
},
"dependencies": {
"dayjs": "^1.10.4",
"nano-markdown": "^1.2.1",
"preact": "^10.5.12",
"preact": "^10.5.13",
"react": "npm:@preact/compat@^0.0.4",
"react-dom": "npm:@preact/compat@^0.0.4"
}
Expand Down
26 changes: 7 additions & 19 deletions packages.dhall
Original file line number Diff line number Diff line change
Expand Up @@ -117,34 +117,22 @@ let additions =
-------------------------------
-}
let upstream =
https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20210226/packages.dhall sha256:7e973070e323137f27e12af93bc2c2f600d53ce4ae73bb51f34eb7d7ce0a43ea

let overrides =
{ simple-json =
upstream.simple-json
// { repo = "https://github.com/robertdp/purescript-simple-json.git"
, version = "v7.0.1"
}
}
https://github.com/purescript/package-sets/releases/download/psc-0.14.0-20210311/packages.dhall sha256:3da8be2b7b4a0e7de6186591167b363023695accffb98a8639e9e7d06e2070d6

let additions =
{ apiary =
{ dependencies = [ "affjax", "media-types", "simple-json" ]
, repo = "https://github.com/robertdp/purescript-apiary"
, version = "v0.2.0"
}
, wire-react-router =
{ web-router =
{ dependencies =
[ "aff"
, "effect"
, "freet"
, "indexed-monad"
, "prelude"
, "profunctor-lenses"
, "react-basic-hooks"
, "routing"
]
, repo = "https://github.com/robertdp/purescript-wire-react-router"
, version = "v0.2.1"
, repo = "https://github.com/robertdp/purescript-web-router.git"
, version = "v0.3.0"
}
}

in upstream // overrides // additions
in upstream // additions
9 changes: 5 additions & 4 deletions spago.dhall
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
{ name = "purescript-react-realword"
, dependencies =
[ "apiary"
[ "affjax"
, "argonaut-codecs"
, "argonaut-core"
, "console"
, "effect"
, "event"
, "foreign-generic"
, "halogen-subscriptions"
, "heterogeneous"
, "js-timers"
, "profunctor-lenses"
Expand All @@ -15,8 +16,8 @@
, "routing"
, "routing-duplex"
, "unicode"
, "web-router"
, "web-uievents"
, "wire-react-router"
]
, packages = ./packages.dhall
, sources = [ "src/**/*.purs", "test/**/*.purs" ]
Expand Down
189 changes: 189 additions & 0 deletions src/Conduit/Api/Client.purs
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
module Conduit.Api.Client where

import Prelude
import Affjax (defaultRequest)
import Affjax as Affjax
import Affjax.RequestBody as RequestBody
import Affjax.RequestHeader (RequestHeader(..))
import Affjax.ResponseFormat as ResponseFormat
import Affjax.ResponseHeader (ResponseHeader)
import Affjax.StatusCode (StatusCode(..))
import Conduit.Api.Endpoint (Endpoint, endpointCodec)
import Conduit.Capability.Auth (class MonadAuth)
import Conduit.Capability.Auth as Auth
import Conduit.Capability.Routing (class MonadRouting)
import Conduit.Capability.Routing as Routing
import Conduit.Config as Config
import Conduit.Data.Route (Route(..))
import Control.Monad.Except (ExceptT(..), except, runExceptT, throwError, withExceptT)
import Data.Argonaut.Core as AC
import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError, decodeJson, printJsonDecodeError)
import Data.Argonaut.Encode (class EncodeJson, encodeJson)
import Data.Array as Array
import Data.Bitraversable (lfor)
import Data.Either (Either(..))
import Data.HTTP.Method (Method)
import Data.Maybe (Maybe(..))
import Data.MediaType.Common (applicationJSON)
import Effect.Aff (Aff)
import Effect.Aff.Class (class MonadAff, liftAff)
import Effect.Class (class MonadEffect)
import Effect.Class.Console as Console
import Routing.Duplex (print)

type URL
= String

type Request
= { method :: Method
, url :: URL
, headers :: Array RequestHeader
, body :: AC.Json
}

type Response
= { status :: StatusCode
, headers :: Array ResponseHeader
, body :: AC.Json
}

data Error
= NotAuthorized
| RuntimeError Affjax.Error
| DecodeError Request Response JsonDecodeError
| UnexpectedResponse Request Response

instance showError :: Show Error where
show NotAuthorized = "(NotAuthorized)"
show (RuntimeError err) = "(RuntimeError {- " <> Affjax.printError err <> " -})"
show (DecodeError req res err) = "(DecodeError " <> printRequest req <> " " <> printResponse res <> " " <> printJsonDecodeError err <> ")"
show (UnexpectedResponse req res) = "(UnexpectedResponse " <> printRequest req <> " " <> printResponse res <> ")"

makeRequest' ::
forall m body response.
MonadAff m =>
EncodeJson body =>
DecodeJson response =>
Method ->
StatusCode ->
Endpoint ->
(Request -> Request) ->
body ->
m (Either Error response)
makeRequest' method statusCode endpoint transform body = liftAff $ runExceptT $ handle =<< fetch request
where
request = transform $ buildRequest method endpoint body

handle resp
| resp.status == statusCode = decode resp
| otherwise = throwError $ UnexpectedResponse request resp

decode resp = withExceptT (DecodeError request resp) $ except $ decodeJson resp.body

makeRequest ::
forall m body response.
MonadAff m =>
EncodeJson body =>
DecodeJson response =>
Method ->
StatusCode ->
Endpoint ->
body ->
m (Either Error response)
makeRequest method statusCode endpoint body = do
res <- makeRequest' method statusCode endpoint addBaseUrl body
void $ lfor res onError
pure res

makeSecureRequest' ::
forall m body response.
MonadAff m =>
EncodeJson body =>
DecodeJson response =>
String ->
Method ->
StatusCode ->
Endpoint ->
body ->
m (Either Error response)
makeSecureRequest' token method statusCode endpoint body = do
res <- makeRequest' method statusCode endpoint (addBaseUrl <<< addToken token) body
void $ lfor res onError
pure res

makeSecureRequest ::
forall m body response.
MonadAuth m =>
MonadRouting m =>
MonadAff m =>
EncodeJson body =>
DecodeJson response =>
Method ->
StatusCode ->
Endpoint ->
body ->
m (Either Error response)
makeSecureRequest method statusCode endpoint body = do
auth <- Auth.read
case auth of
Nothing -> do
Routing.redirect Register
pure $ Left $ NotAuthorized
Just { token } -> do
makeSecureRequest' token method statusCode endpoint body

buildRequest :: forall body. EncodeJson body => Method -> Endpoint -> body -> Request
buildRequest method endpoint body =
{ method
, url: print endpointCodec endpoint
, headers: [ ContentType applicationJSON ]
, body: encodeJson body
}

fetch :: Request -> ExceptT Error Aff Response
fetch { method, url, headers, body } = do
response <- withExceptT RuntimeError $ ExceptT runRequest
pure
{ status: response.status
, headers: response.headers
, body: response.body
}
where
runRequest =
Affjax.request
$ defaultRequest
{ method = Left method
, url = url
, headers = headers
, responseFormat = ResponseFormat.json
, content = if AC.isNull body then Nothing else pure $ RequestBody.json body
}

addBaseUrl :: forall r. { url :: String | r } -> { url :: String | r }
addBaseUrl request@{ url } = request { url = Config.apiEndpoint <> url }

addToken :: forall r. String -> { headers :: Array RequestHeader | r } -> { headers :: Array RequestHeader | r }
addToken token request@{ headers } = request { headers = Array.snoc headers (RequestHeader "Authorization" ("Token " <> token)) }

onError :: forall m. MonadEffect m => Error -> m Unit
onError error = do
when (Config.nodeEnv /= "production") do
Console.log $ show error

isNotFound :: forall response. Either Error response -> Boolean
isNotFound = case _ of
Left (UnexpectedResponse _ { status })
| status == StatusCode 404 -> true
_ -> false

isUnprocessableEntity :: forall response. Either Error response -> Boolean
isUnprocessableEntity = case _ of
Left (UnexpectedResponse _ { status })
| status == StatusCode 422 -> true
_ -> false

printRequest :: Request -> String
printRequest req@{ body } = show $ req { body = AC.stringify body }

printResponse :: Response -> String
printResponse res@{ body } = show $ res { body = AC.stringify body }
Loading

0 comments on commit 596cbaa

Please sign in to comment.