-
-
Save williammartin/6b1c46711dd484042b4441a284a9b22a to your computer and use it in GitHub Desktop.
| import * as core from '@actions/core'; | |
| import { Storage } from '@google-cloud/storage'; | |
| import * as E from 'fp-ts/lib/Either'; | |
| import { pipe } from 'fp-ts/lib/function'; | |
| import * as IOEither from 'fp-ts/lib/IOEither'; | |
| import * as T from 'fp-ts/lib/Task'; | |
| import * as TE from 'fp-ts/lib/TaskEither'; | |
| import * as t from 'io-ts'; | |
| import { failure } from 'io-ts/PathReporter'; | |
| const ParsedCredentials = t.type({ | |
| type: t.string, | |
| project_id: t.string, | |
| private_key_id: t.string, | |
| private_key: t.string, | |
| client_email: t.string, | |
| client_id: t.string, | |
| auth_uri: t.string, | |
| token_uri: t.string, | |
| auth_provider_x509_cert_url: t.string, | |
| client_x509_cert_url: t.string, | |
| }); | |
| type ParsedCredentials = t.TypeOf<typeof ParsedCredentials>; | |
| // Utility to convert caught "things" of unknown shape into Errors | |
| const unknownReasonAsError = (reason: unknown) => | |
| reason instanceof Error ? reason : new Error(String(reason)); | |
| // Parse JSON into Either | |
| const safeParseJSON = E.tryCatchK(JSON.parse, unknownReasonAsError); | |
| // IO wrapper around loading GitHub Actions Inputs | |
| // which load from Environment Variables | |
| const safeGetInput = (input: string) => | |
| IOEither.tryCatch( | |
| () => core.getInput(input, { required: true }), | |
| unknownReasonAsError, | |
| ); | |
| const parseCredentials = (serialisedMaybeCredentials: string) => | |
| pipe( | |
| serialisedMaybeCredentials, | |
| safeParseJSON, | |
| E.chainW(ParsedCredentials.decode), | |
| E.mapLeft( | |
| (e) => | |
| new Error( | |
| `failed to parse credentials because: ${ | |
| e instanceof Error ? e : failure(e).join('\n') | |
| }`, | |
| ), | |
| ), | |
| ); | |
| // Wrapper around creating Google Cloud Storage client | |
| const createStorage = (credentials: ParsedCredentials) => { | |
| return new Storage({ | |
| userAgent: 'delete-gcs-bucket-contents/0.0.1', | |
| credentials, | |
| }); | |
| }; | |
| // Empty the Bucket | |
| const emptyBucket = (storage: Storage) => (bucket: string) => | |
| pipe( | |
| bucket, | |
| TE.tryCatchK( | |
| (bucket: string) => storage.bucket(bucket).deleteFiles(), | |
| unknownReasonAsError, | |
| ), | |
| ); | |
| const run: T.Task<void> = pipe( | |
| // This nested pipe seems like something that should be removable, but I'm not sure how? | |
| pipe(safeGetInput('credentials'), IOEither.chainEitherK(parseCredentials)), | |
| IOEither.map(createStorage), | |
| TE.fromIOEither, | |
| TE.chain((storage) => | |
| // It would be nice if I could avoid the closing over `storage` and instead flow it somehow? | |
| // Again, another nested pipe I'm not sure about | |
| // Also, I lifted fromIOEither in two places which seems like it might be funny? | |
| pipe( | |
| safeGetInput('bucket'), | |
| TE.fromIOEither, | |
| TE.chain(emptyBucket(storage)), | |
| ), | |
| ), | |
| // This map seems a bit weird, especially with the right side that looks like a unit... | |
| // I was trying to follow patterns from: https://dev.to/anthonyjoeseph/should-i-use-fp-ts-task-h52 | |
| T.map( | |
| E.fold( | |
| (error) => { | |
| throw error; | |
| }, | |
| () => {}, | |
| ), | |
| ), | |
| ); | |
| pipe(run, (invoke) => invoke()); |
Awesome thank you so much @cdimitroulas
Regarding the first point, I was looking at https://dev.to/anthonyjoeseph/should-i-use-fp-ts-task-h52#rule-number-3 which recommends:
You should especially avoid invoking a TaskEither.
Which looks like what is happening in your proposal (though I do like keeping the error handling at the top level). What do you think about this?
I tried out the Do notation in a different way (binding earlier for the inputs) but it didn't seem so nice. This looks way better.
Thanks again!
I had a look at the rule in the article you linked to. I think there is some merit to that rule and it may help in certain circumstances to enforce error handling.
Personally I'm not a big fan of enforcing everything to be Task<void>. Often in my real work codebase I would be invoking a Task in an Express route handler which calls the core logic, then converts the success/error to the relevant HTTP response. In those cases I usually end up invoking a Task<Response> (where Response is from the express library), and I wouldn't want to make that a Task<void> as it correctly forces the code to actually return a response in all possible scenarios.
Something like this:
const handler: Handler = (req, res) => {
return pipe(
myTaskEither(),
TE.fold(
(err) => {
switch (err._tag) {
case "NotFound":
return T.of(res.status(404).send({ msg: "Thing not found" }))
case "UnexpectedError":
return T.of(res.sendStatus(500))
}
},
(result) => T.of(res.status(200).send(result))
)
)() // <-- invoke it here
}
I would run the main
Taskslightly differently, the finalpipeis a bit unnecessary.You can just write:
In terms of the
T.map(E.fold(...)), you could remove this entirely and then leave the error handling up to the top level:You can often use
bindandbindToto reduce nested pipes. See the do notation docs for a bit more info on how to use these.Here's a "flatter"
runfunction that I was able to come up with: