all repos

onasty @ d4cfa0b7a2d8f751426feb4210534dc907302ad1

a one-time notes service

onasty/web/src/Effect.elm (view raw)

Olexandr Smirnov Olexandr Smirnov
ss2316544@gmail.com
web: general refactor (#158)..., 11 months ago
1
module Effect exposing
2
    ( Effect, none, batch, sendCmd, sendMsg
3
    , pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back
4
    , sendApiRequest, sendToClipboard
5
    , signin, logout, refreshTokens, saveUser, clearUser
6
    , map, toCmd
7
    )
8
9
{-|
10
11
@docs Effect, none, batch, sendCmd, sendMsg
12
@docs pushRoute, replaceRoute, pushRoutePath, replaceRoutePath, loadExternalUrl, back
13
@docs sendApiRequest, sendToClipboard
14
@docs signin, logout, refreshTokens, saveUser, clearUser
15
@docs map, toCmd
16
17
-}
18
19
import Api
20
import Auth.User
21
import Browser.Navigation
22
import Data.Credentials exposing (Credentials)
23
import Data.Error
24
import Dict exposing (Dict)
25
import Http
26
import Json.Decode
27
import Json.Encode
28
import Ports
29
import Route
30
import Route.Path
31
import Shared.Model
32
import Shared.Msg
33
import Task
34
import Url exposing (Url)
35
36
37
type Effect msg
38
    = -- BASICS
39
      None
40
    | Batch (List (Effect msg))
41
    | SendCmd (Cmd msg)
42
      -- ROUTING
43
    | PushUrl String
44
    | ReplaceUrl String
45
    | LoadExternalUrl String
46
    | Back
47
      -- SHARED
48
    | SendSharedMsg Shared.Msg.Msg
49
    | SendToLocalStorage { key : String, value : Json.Encode.Value }
50
    | SendToClipboard String
51
    | SendApiRequest
52
        { endpoint : String
53
        , method : String
54
        , body : Http.Body
55
        , decoder : Json.Decode.Decoder msg
56
        , onHttpError : Api.Error -> msg
57
        }
58
59
60
61
-- BASICS
62
63
64
{-| Don't send any effect.
65
-}
66
none : Effect msg
67
none =
68
    None
69
70
71
{-| Send multiple effects at once.
72
-}
73
batch : List (Effect msg) -> Effect msg
74
batch =
75
    Batch
76
77
78
{-| Send a normal `Cmd msg` as an effect, something like `Http.get` or `Random.generate`.
79
-}
80
sendCmd : Cmd msg -> Effect msg
81
sendCmd =
82
    SendCmd
83
84
85
{-| Send a message as an effect. Useful when emitting events from UI components.
86
-}
87
sendMsg : msg -> Effect msg
88
sendMsg msg =
89
    Task.succeed msg
90
        |> Task.perform identity
91
        |> SendCmd
92
93
94
95
-- ROUTING
96
97
98
{-| Set the new route, and make the back button go back to the current route.
99
-}
100
pushRoute :
101
    { path : Route.Path.Path
102
    , query : Dict String String
103
    , hash : Maybe String
104
    }
105
    -> Effect msg
106
pushRoute route =
107
    PushUrl (Route.toString route)
108
109
110
{-| Same as `Effect.pushRoute`, but without `query` or `hash` support
111
-}
112
pushRoutePath : Route.Path.Path -> Effect msg
113
pushRoutePath path =
114
    PushUrl (Route.Path.toString path)
115
116
117
{-| Set the new route, but replace the previous one, so clicking the back
118
button **won't** go back to the previous route.
119
-}
120
replaceRoute :
121
    { path : Route.Path.Path
122
    , query : Dict String String
123
    , hash : Maybe String
124
    }
125
    -> Effect msg
126
replaceRoute route =
127
    ReplaceUrl (Route.toString route)
128
129
130
{-| Same as `Effect.replaceRoute`, but without `query` or `hash` support
131
-}
132
replaceRoutePath : Route.Path.Path -> Effect msg
133
replaceRoutePath path =
134
    ReplaceUrl (Route.Path.toString path)
135
136
137
{-| Redirect users to a new URL, somewhere external to your web application.
138
-}
139
loadExternalUrl : String -> Effect msg
140
loadExternalUrl =
141
    LoadExternalUrl
142
143
144
{-| Navigate back one page
145
-}
146
back : Effect msg
147
back =
148
    Back
149
150
151
152
-- SHARED
153
154
155
sendApiRequest :
156
    { endpoint : String
157
    , method : String
158
    , body : Http.Body
159
    , decoder : Json.Decode.Decoder value
160
    , onResponse : Result Api.Error value -> msg
161
    }
162
    -> Effect msg
163
sendApiRequest opts =
164
    SendApiRequest
165
        { endpoint = opts.endpoint
166
        , method = opts.method
167
        , body = opts.body
168
        , onHttpError = \e -> opts.onResponse (Err e)
169
        , decoder =
170
            opts.decoder
171
                |> Json.Decode.map Ok
172
                |> Json.Decode.map opts.onResponse
173
        }
174
175
176
sendToClipboard : String -> Effect msg
177
sendToClipboard text =
178
    SendToClipboard text
179
180
181
refreshTokens : Effect msg
182
refreshTokens =
183
    SendSharedMsg Shared.Msg.TriggerTokenRefresh
184
185
186
signin : Credentials -> Effect msg
187
signin credentials =
188
    SendSharedMsg (Shared.Msg.SignedIn credentials)
189
190
191
logout : Effect msg
192
logout =
193
    SendSharedMsg Shared.Msg.Logout
194
195
196
saveUser : String -> String -> Effect msg
197
saveUser accessToken refreshToken =
198
    batch
199
        [ SendToLocalStorage { key = "access_token", value = Json.Encode.string accessToken }
200
        , SendToLocalStorage { key = "refresh_token", value = Json.Encode.string refreshToken }
201
        ]
202
203
204
clearUser : Effect msg
205
clearUser =
206
    batch
207
        [ SendToLocalStorage { key = "access_token", value = Json.Encode.null }
208
        , SendToLocalStorage { key = "refresh_token", value = Json.Encode.null }
209
        ]
210
211
212
213
-- INTERNALS
214
215
216
map : (msg1 -> msg2) -> Effect msg1 -> Effect msg2
217
map fn effect =
218
    case effect of
219
        None ->
220
            None
221
222
        Batch list ->
223
            Batch (List.map (map fn) list)
224
225
        SendCmd cmd ->
226
            SendCmd (Cmd.map fn cmd)
227
228
        PushUrl url ->
229
            PushUrl url
230
231
        ReplaceUrl url ->
232
            ReplaceUrl url
233
234
        Back ->
235
            Back
236
237
        LoadExternalUrl url ->
238
            LoadExternalUrl url
239
240
        SendSharedMsg sharedMsg ->
241
            SendSharedMsg sharedMsg
242
243
        SendToLocalStorage options ->
244
            SendToLocalStorage options
245
246
        SendToClipboard text ->
247
            SendToClipboard text
248
249
        SendApiRequest opts ->
250
            SendApiRequest
251
                { endpoint = opts.endpoint
252
                , method = opts.method
253
                , body = opts.body
254
                , decoder = Json.Decode.map fn opts.decoder
255
                , onHttpError = \err -> fn (opts.onHttpError err)
256
                }
257
258
259
toCmd :
260
    { key : Browser.Navigation.Key
261
    , url : Url
262
    , shared : Shared.Model.Model
263
    , fromSharedMsg : Shared.Msg.Msg -> msg
264
    , batch : List msg -> msg
265
    , toCmd : msg -> Cmd msg
266
    }
267
    -> Effect msg
268
    -> Cmd msg
269
toCmd options effect =
270
    case effect of
271
        None ->
272
            Cmd.none
273
274
        Batch list ->
275
            Cmd.batch (List.map (toCmd options) list)
276
277
        SendCmd cmd ->
278
            cmd
279
280
        PushUrl url ->
281
            Browser.Navigation.pushUrl options.key url
282
283
        ReplaceUrl url ->
284
            Browser.Navigation.replaceUrl options.key url
285
286
        Back ->
287
            Browser.Navigation.back options.key 1
288
289
        LoadExternalUrl url ->
290
            Browser.Navigation.load url
291
292
        SendSharedMsg sharedMsg ->
293
            Task.succeed sharedMsg
294
                |> Task.perform options.fromSharedMsg
295
296
        SendToLocalStorage opts ->
297
            Ports.sendToLocalStorage opts
298
299
        SendToClipboard text ->
300
            Ports.sendToClipboard text
301
302
        SendApiRequest opts ->
303
            let
304
                headers : List Http.Header
305
                headers =
306
                    case options.shared.user of
307
                        Auth.User.SignedIn cred ->
308
                            if not (String.contains opts.endpoint "refresh-tokens") then
309
                                [ Http.header "Authorization" ("Bearer " ++ cred.accessToken) ]
310
311
                            else
312
                                []
313
314
                        _ ->
315
                            []
316
            in
317
            Http.request
318
                { method = opts.method
319
                , url = opts.endpoint
320
                , headers = headers
321
                , body = opts.body
322
                , expect =
323
                    Http.expectStringResponse
324
                        (\httpResult ->
325
                            case httpResult of
326
                                Ok msg ->
327
                                    msg
328
329
                                Err err ->
330
                                    opts.onHttpError err
331
                        )
332
                        (\resp -> httpResponseToCustomError opts.decoder resp)
333
                , timeout = Just (1000 * 60) -- 60 second timeout
334
                , tracker = Nothing
335
                }
336
337
338
httpResponseToCustomError : Json.Decode.Decoder msg -> Http.Response String -> Result Api.Error msg
339
httpResponseToCustomError decoder response =
340
    case response of
341
        Http.GoodStatus_ _ body ->
342
            case
343
                Json.Decode.decodeString decoder
344
                    (if String.isEmpty body then
345
                        "\"\""
346
347
                     else
348
                        body
349
                    )
350
            of
351
                Ok value ->
352
                    Ok value
353
354
                Err err ->
355
                    Err (Api.JsonDecodeError { message = "Failed to decode response", reason = err })
356
357
        Http.BadStatus_ { statusCode } body ->
358
            case body of
359
                "" ->
360
                    Err (Api.HttpError { message = "Unexpected empty response", reason = Http.BadStatus statusCode })
361
362
                _ ->
363
                    case Json.Decode.decodeString Data.Error.decode body of
364
                        Ok err ->
365
                            Err (Api.HttpError { message = err.message, reason = Http.BadStatus statusCode })
366
367
                        Err err ->
368
                            Err (Api.JsonDecodeError { message = "Failed to decode response", reason = err })
369
370
        Http.BadUrl_ url ->
371
            Err (Api.HttpError { message = "Unexpected URL format", reason = Http.BadUrl url })
372
373
        Http.Timeout_ ->
374
            Err (Api.HttpError { message = "Request timed out, please try again", reason = Http.Timeout })
375
376
        Http.NetworkError_ ->
377
            Err (Api.HttpError { message = "Could not connect, please try again", reason = Http.NetworkError })