Skip to content

Instantly share code, notes, and snippets.

@johtso
Created July 4, 2024 09:57
Show Gist options
  • Save johtso/922f54bf0a510b417578ffeab5553128 to your computer and use it in GitHub Desktop.
Save johtso/922f54bf0a510b417578ffeab5553128 to your computer and use it in GitHub Desktop.
One-shot error decoding from full response using a single union Effect schema
import { Schema as S, AST } from "@effect/schema"
import * as Errors from "./errors"
import { Either, Option } from "effect"
const overwriteTag = <Tag extends AST.LiteralValue>(tag: Tag) =>
S.optionalToRequired(S.Unknown, S.Literal(tag), {
decode: () => tag,
encode: () => Option.none()
}).pipe(S.withConstructorDefault(() => tag))
const ErrorBody = S.Struct({
_tag: overwriteTag("ErrorBody"),
ErrorCode: S.Number.pipe(S.nonNegative(), S.int()),
Message: S.String
})
const PostmarkErrorSchema = S.Union(
...Object.values(Errors).map((error) => S.instanceOf(error))
)
type AnyError = typeof Errors[keyof typeof Errors]
const ErrorResponse = <A, I, R, A2 extends number, I2>(BodySchema: S.Schema<A, I, R>, statusCode: S.Schema<A2, I2>, error: AnyError, specificErrorMap?: Record<number, AnyError>) =>
S.Struct({
status: statusCode,
body: S.Union(ErrorBody, S.String)
}).pipe(S.transform(
S.EitherFromSelf({ right: S.typeSchema(BodySchema), left: PostmarkErrorSchema }),
{
decode: (resp) => {
if (typeof resp.body === "string") {
return Either.left(new error({ statusCode: resp.status }))
} else {
const e = specificErrorMap?.[resp.body.ErrorCode] ?? error
return Either.left(new e({ statusCode: resp.status, errorCode: resp.body.ErrorCode, message: resp.body.Message }))
}
},
encode: (_) => null as any,
}
))
const ResponseSchema = <A, I, R>(BodySchema: S.Schema<A, I, R>) => S.Union(
// Success
S.Struct({
status: S.Literal(200),
body: BodySchema,
}).pipe(S.transform(
S.EitherFromSelf({ right: S.typeSchema(BodySchema), left: PostmarkErrorSchema }),
{
decode: (resp) => Either.right(resp.body),
encode: (_) => null as any
}
)),
// Specifically handled error codes
ErrorResponse(BodySchema, S.Literal(401), Errors.PostmarkInvalidAPIKeyError),
ErrorResponse(BodySchema, S.Literal(422), Errors.PostmarkUnprocessableEntityError, { 406: Errors.PostmarkInactiveRecipientError }),
// Any other error
ErrorResponse(BodySchema, S.Number, Errors.PostmarkError),
)
export const SendEmailSuccessBody = S.Struct({
_tag: overwriteTag("SendEmailSuccessBody"),
To: S.String,
SubmittedAt: S.String,
MessageID: S.String,
ErrorCode: S.Literal(0),
Message: S.String
})
export const SendEmailResponseSchema = ResponseSchema(SendEmailSuccessBody)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment