-
-
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
Task
slightly differently, the finalpipe
is 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
bind
andbindTo
to reduce nested pipes. See the do notation docs for a bit more info on how to use these.Here's a "flatter"
run
function that I was able to come up with: