Skip to content

Commit 596cbaa

Browse files
authored
Refactor and update for PS 0.14 (#18)
* 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
1 parent 796327f commit 596cbaa

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+1330
-1528
lines changed

README.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,18 +52,14 @@ An implementation of React hooks on top of purescript-react-basic.
5252

5353
A Halogen-inspired interface for React.
5454

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

57-
A basic pushstate router for React, with support for asynchronous routing logic.
57+
A basic web router with support for asynchronous routing logic.
5858

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

6161
Unified parsing and printing for routes in PureScript.
6262

63-
#### [Apiary](https://github.com/robertdp/purescript-apiary)
64-
65-
For the creation of type-level specs that can be queried against automatically.
66-
6763
## Recognition
6864

6965
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).

index.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@
77
<title>Conduit</title>
88
<link
99
href="//code.ionicframework.com/ionicons/2.0.1/css/ionicons.min.css"
10-
rel="preload"
10+
rel="stylesheet"
1111
media="print"
1212
onload="this.media='all'; this.onload=null;"
1313
/>
1414
<link
1515
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"
16-
rel="preload"
16+
rel="stylesheet"
1717
media="print"
1818
onload="this.media='all'; this.onload=null;"
1919
/>

package.json

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "purescript-react-realworld",
3-
"version": "1.0.0",
3+
"version": "3.0.0",
44
"description": "A real-world application demonstrating PureScript and React",
55
"keywords": [
66
"Purescript",
@@ -9,7 +9,8 @@
99
],
1010
"contributors": [
1111
{
12-
"name": "Jonas Buntinx"
12+
"name": "Jonas Buntinx",
13+
"url": "https://github.com/jonasbuntinx"
1314
},
1415
{
1516
"name": "Robert Porter",
@@ -30,17 +31,17 @@
3031
"test": "spago test --no-install"
3132
},
3233
"devDependencies": {
33-
"parcel": "^1.12.4",
34-
"purescript": "^0.13.8",
34+
"parcel": "1.12.3",
35+
"purescript": "^0.14.0",
3536
"purescript-psa": "^0.8.2",
36-
"purty": "^6.3.1",
37+
"purty": "^7.0.0",
3738
"spago": "^0.19.1",
3839
"zephyr": "https://github.com/jonasbuntinx/zephyr.git"
3940
},
4041
"dependencies": {
4142
"dayjs": "^1.10.4",
4243
"nano-markdown": "^1.2.1",
43-
"preact": "^10.5.12",
44+
"preact": "^10.5.13",
4445
"react": "npm:@preact/compat@^0.0.4",
4546
"react-dom": "npm:@preact/compat@^0.0.4"
4647
}

packages.dhall

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -117,34 +117,22 @@ let additions =
117117
-------------------------------
118118
-}
119119
let upstream =
120-
https://github.com/purescript/package-sets/releases/download/psc-0.13.8-20210226/packages.dhall sha256:7e973070e323137f27e12af93bc2c2f600d53ce4ae73bb51f34eb7d7ce0a43ea
121-
122-
let overrides =
123-
{ simple-json =
124-
upstream.simple-json
125-
// { repo = "https://github.com/robertdp/purescript-simple-json.git"
126-
, version = "v7.0.1"
127-
}
128-
}
120+
https://github.com/purescript/package-sets/releases/download/psc-0.14.0-20210311/packages.dhall sha256:3da8be2b7b4a0e7de6186591167b363023695accffb98a8639e9e7d06e2070d6
129121

130122
let additions =
131-
{ apiary =
132-
{ dependencies = [ "affjax", "media-types", "simple-json" ]
133-
, repo = "https://github.com/robertdp/purescript-apiary"
134-
, version = "v0.2.0"
135-
}
136-
, wire-react-router =
123+
{ web-router =
137124
{ dependencies =
138125
[ "aff"
126+
, "effect"
139127
, "freet"
140128
, "indexed-monad"
129+
, "prelude"
141130
, "profunctor-lenses"
142-
, "react-basic-hooks"
143131
, "routing"
144132
]
145-
, repo = "https://github.com/robertdp/purescript-wire-react-router"
146-
, version = "v0.2.1"
133+
, repo = "https://github.com/robertdp/purescript-web-router.git"
134+
, version = "v0.3.0"
147135
}
148136
}
149137

150-
in upstream // overrides // additions
138+
in upstream // additions

spago.dhall

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{ name = "purescript-react-realword"
22
, dependencies =
3-
[ "apiary"
3+
[ "affjax"
4+
, "argonaut-codecs"
5+
, "argonaut-core"
46
, "console"
57
, "effect"
6-
, "event"
7-
, "foreign-generic"
8+
, "halogen-subscriptions"
89
, "heterogeneous"
910
, "js-timers"
1011
, "profunctor-lenses"
@@ -15,8 +16,8 @@
1516
, "routing"
1617
, "routing-duplex"
1718
, "unicode"
19+
, "web-router"
1820
, "web-uievents"
19-
, "wire-react-router"
2021
]
2122
, packages = ./packages.dhall
2223
, sources = [ "src/**/*.purs", "test/**/*.purs" ]

src/Conduit/Api/Client.purs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
module Conduit.Api.Client where
2+
3+
import Prelude
4+
import Affjax (defaultRequest)
5+
import Affjax as Affjax
6+
import Affjax.RequestBody as RequestBody
7+
import Affjax.RequestHeader (RequestHeader(..))
8+
import Affjax.ResponseFormat as ResponseFormat
9+
import Affjax.ResponseHeader (ResponseHeader)
10+
import Affjax.StatusCode (StatusCode(..))
11+
import Conduit.Api.Endpoint (Endpoint, endpointCodec)
12+
import Conduit.Capability.Auth (class MonadAuth)
13+
import Conduit.Capability.Auth as Auth
14+
import Conduit.Capability.Routing (class MonadRouting)
15+
import Conduit.Capability.Routing as Routing
16+
import Conduit.Config as Config
17+
import Conduit.Data.Route (Route(..))
18+
import Control.Monad.Except (ExceptT(..), except, runExceptT, throwError, withExceptT)
19+
import Data.Argonaut.Core as AC
20+
import Data.Argonaut.Decode (class DecodeJson, JsonDecodeError, decodeJson, printJsonDecodeError)
21+
import Data.Argonaut.Encode (class EncodeJson, encodeJson)
22+
import Data.Array as Array
23+
import Data.Bitraversable (lfor)
24+
import Data.Either (Either(..))
25+
import Data.HTTP.Method (Method)
26+
import Data.Maybe (Maybe(..))
27+
import Data.MediaType.Common (applicationJSON)
28+
import Effect.Aff (Aff)
29+
import Effect.Aff.Class (class MonadAff, liftAff)
30+
import Effect.Class (class MonadEffect)
31+
import Effect.Class.Console as Console
32+
import Routing.Duplex (print)
33+
34+
type URL
35+
= String
36+
37+
type Request
38+
= { method :: Method
39+
, url :: URL
40+
, headers :: Array RequestHeader
41+
, body :: AC.Json
42+
}
43+
44+
type Response
45+
= { status :: StatusCode
46+
, headers :: Array ResponseHeader
47+
, body :: AC.Json
48+
}
49+
50+
data Error
51+
= NotAuthorized
52+
| RuntimeError Affjax.Error
53+
| DecodeError Request Response JsonDecodeError
54+
| UnexpectedResponse Request Response
55+
56+
instance showError :: Show Error where
57+
show NotAuthorized = "(NotAuthorized)"
58+
show (RuntimeError err) = "(RuntimeError {- " <> Affjax.printError err <> " -})"
59+
show (DecodeError req res err) = "(DecodeError " <> printRequest req <> " " <> printResponse res <> " " <> printJsonDecodeError err <> ")"
60+
show (UnexpectedResponse req res) = "(UnexpectedResponse " <> printRequest req <> " " <> printResponse res <> ")"
61+
62+
makeRequest' ::
63+
forall m body response.
64+
MonadAff m =>
65+
EncodeJson body =>
66+
DecodeJson response =>
67+
Method ->
68+
StatusCode ->
69+
Endpoint ->
70+
(Request -> Request) ->
71+
body ->
72+
m (Either Error response)
73+
makeRequest' method statusCode endpoint transform body = liftAff $ runExceptT $ handle =<< fetch request
74+
where
75+
request = transform $ buildRequest method endpoint body
76+
77+
handle resp
78+
| resp.status == statusCode = decode resp
79+
| otherwise = throwError $ UnexpectedResponse request resp
80+
81+
decode resp = withExceptT (DecodeError request resp) $ except $ decodeJson resp.body
82+
83+
makeRequest ::
84+
forall m body response.
85+
MonadAff m =>
86+
EncodeJson body =>
87+
DecodeJson response =>
88+
Method ->
89+
StatusCode ->
90+
Endpoint ->
91+
body ->
92+
m (Either Error response)
93+
makeRequest method statusCode endpoint body = do
94+
res <- makeRequest' method statusCode endpoint addBaseUrl body
95+
void $ lfor res onError
96+
pure res
97+
98+
makeSecureRequest' ::
99+
forall m body response.
100+
MonadAff m =>
101+
EncodeJson body =>
102+
DecodeJson response =>
103+
String ->
104+
Method ->
105+
StatusCode ->
106+
Endpoint ->
107+
body ->
108+
m (Either Error response)
109+
makeSecureRequest' token method statusCode endpoint body = do
110+
res <- makeRequest' method statusCode endpoint (addBaseUrl <<< addToken token) body
111+
void $ lfor res onError
112+
pure res
113+
114+
makeSecureRequest ::
115+
forall m body response.
116+
MonadAuth m =>
117+
MonadRouting m =>
118+
MonadAff m =>
119+
EncodeJson body =>
120+
DecodeJson response =>
121+
Method ->
122+
StatusCode ->
123+
Endpoint ->
124+
body ->
125+
m (Either Error response)
126+
makeSecureRequest method statusCode endpoint body = do
127+
auth <- Auth.read
128+
case auth of
129+
Nothing -> do
130+
Routing.redirect Register
131+
pure $ Left $ NotAuthorized
132+
Just { token } -> do
133+
makeSecureRequest' token method statusCode endpoint body
134+
135+
buildRequest :: forall body. EncodeJson body => Method -> Endpoint -> body -> Request
136+
buildRequest method endpoint body =
137+
{ method
138+
, url: print endpointCodec endpoint
139+
, headers: [ ContentType applicationJSON ]
140+
, body: encodeJson body
141+
}
142+
143+
fetch :: Request -> ExceptT Error Aff Response
144+
fetch { method, url, headers, body } = do
145+
response <- withExceptT RuntimeError $ ExceptT runRequest
146+
pure
147+
{ status: response.status
148+
, headers: response.headers
149+
, body: response.body
150+
}
151+
where
152+
runRequest =
153+
Affjax.request
154+
$ defaultRequest
155+
{ method = Left method
156+
, url = url
157+
, headers = headers
158+
, responseFormat = ResponseFormat.json
159+
, content = if AC.isNull body then Nothing else pure $ RequestBody.json body
160+
}
161+
162+
addBaseUrl :: forall r. { url :: String | r } -> { url :: String | r }
163+
addBaseUrl request@{ url } = request { url = Config.apiEndpoint <> url }
164+
165+
addToken :: forall r. String -> { headers :: Array RequestHeader | r } -> { headers :: Array RequestHeader | r }
166+
addToken token request@{ headers } = request { headers = Array.snoc headers (RequestHeader "Authorization" ("Token " <> token)) }
167+
168+
onError :: forall m. MonadEffect m => Error -> m Unit
169+
onError error = do
170+
when (Config.nodeEnv /= "production") do
171+
Console.log $ show error
172+
173+
isNotFound :: forall response. Either Error response -> Boolean
174+
isNotFound = case _ of
175+
Left (UnexpectedResponse _ { status })
176+
| status == StatusCode 404 -> true
177+
_ -> false
178+
179+
isUnprocessableEntity :: forall response. Either Error response -> Boolean
180+
isUnprocessableEntity = case _ of
181+
Left (UnexpectedResponse _ { status })
182+
| status == StatusCode 422 -> true
183+
_ -> false
184+
185+
printRequest :: Request -> String
186+
printRequest req@{ body } = show $ req { body = AC.stringify body }
187+
188+
printResponse :: Response -> String
189+
printResponse res@{ body } = show $ res { body = AC.stringify body }

0 commit comments

Comments
 (0)