Skip to content

Commit

Permalink
improvement: validate response
Browse files Browse the repository at this point in the history
  • Loading branch information
namgold committed Dec 14, 2023
1 parent f3b5f69 commit f16db06
Show file tree
Hide file tree
Showing 3 changed files with 125 additions and 14 deletions.
11 changes: 11 additions & 0 deletions src/constants/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { FetchBaseQueryMeta } from '@reduxjs/toolkit/dist/query'
import { captureException } from '@sentry/react'

export class ApiValidateError extends Error {
constructor(res: any, meta: FetchBaseQueryMeta | undefined) {
super('Data error. Please try again later.')
const e = new Error(`[API validate fail] ${meta?.request.url}`)
e.name = 'APIValidateFail'
captureException(e, { level: 'error', extra: { res, meta } })
}
}
33 changes: 19 additions & 14 deletions src/services/blackjack.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,39 @@
import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

import { BLACKJACK_API } from 'constants/env'
import { ApiValidateError } from 'constants/errors'
import { isArray, isBoolean, isNumber, isString, isStruct } from 'utils/validate'

type BlackjackCheck = {
blacklisted: boolean
expiryMs: string
reason: number
}

type BlackjackResponse = {
data: {
wallets: BlackjackCheck[]
}
}
const verifyBlackjackResponse = isStruct({
data: isStruct({
wallets: isArray(
isStruct({
blacklisted: isBoolean,
updatedMs: isString,
reason: isNumber,
}),
),
}),
})

const blackjackApi = createApi({
reducerPath: 'blackjackApi',
baseQuery: fetchBaseQuery({
baseUrl: `${BLACKJACK_API}/v1`,
}),
endpoints: builder => ({
checkBlackjack: builder.query<BlackjackCheck, string>({
checkBlackjack: builder.query({
query: (address: string) => ({
url: '/check',
params: {
wallets: address,
},
}),
transformResponse: (res: BlackjackResponse): BlackjackCheck => {
return res.data.wallets[0]
transformResponse: (res: unknown, meta) => {
if (verifyBlackjackResponse(res)) {
return res.data.wallets[0]
}
throw new ApiValidateError(res, meta)
},
}),
}),
Expand Down
95 changes: 95 additions & 0 deletions src/utils/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* eslint-disable @typescript-eslint/ban-ts-comment */

type GuardedType<T> = T extends (x: any) => x is infer U ? U : never

function isNull(input: unknown): input is null | undefined {
if (input === undefined) return true
if (input === null) return true
return false
}
export function isNumber(input: unknown): input is number {
return typeof input === 'number'
}

export function isBoolean(input: unknown | any): input is boolean {
return typeof input === 'boolean'
}

export function isString(input: unknown): input is string {
return typeof input === 'string'
}

// @ts-ignore
export function isArray<T extends VerifierTypes>(elementVerifier: T): (input: unknown) => input is GuardedType<T>[] {
return (input: unknown): input is GuardedType<T>[] => {
if (!Array.isArray(input)) return false
// @ts-ignore
return input.every(elementVerifier)
}
}

export function isStruct<T extends { [key: string]: VerifierTypes }, U extends keyof T>(
struct: T,
): (input: unknown) => input is { readonly [keys in U]: GuardedType<T[keys]> } {
const verifiers = Object.entries(struct)

return (input: unknown): input is { [keys in U]: GuardedType<T[keys]> } => {
if (typeof input !== 'object') return false
if (isNull(input)) return false
return verifiers.every(([key, verify]) => verify((input as Record<string, any>)[key]))
}
}

// @ts-ignore
type VerifierTypes =
| typeof isString
| typeof isNumber
| typeof isBoolean
| ReturnType<typeof isArray>
| ReturnType<typeof isStruct>

/**
* Use for fields that acceptable to be `undefined` or `null`
*
* Do not use this for fields expected to be non-null to be run correctly, e.g: `response.data`
*
* If you are using nesting, it's likely you've used it incorrectly.
*/
export function isNullable<T extends VerifierTypes>(
verifier: T,
): (input: unknown) => input is null | undefined | GuardedType<T> {
return (input: unknown): input is null | undefined | GuardedType<T> => {
if (input === undefined) return true
if (input === null) return true
return verifier(input)
}
}

/* eslint-disable @typescript-eslint/no-unused-vars */
const ValidateBalanceExample = isArray(
isStruct({
result: isNullable(isArray(isString)),
}),
)

const ValidateComplexResponseExample = isStruct({
data: isStruct({
wallets: isArray(
isStruct({
address: isString,
ens: isNullable(isString),
ETH: isNumber,
reward: isNullable(isNumber),
txs: isArray(isStruct({ id: isString })),
balances: isNullable(ValidateBalanceExample),
}),
),
pagination: isStruct({
pagination: isNumber,
totalItems: isNumber,
}),
}),
})

type ExtractedBalances = GuardedType<typeof ValidateBalanceExample>
/* eslint-enable @typescript-eslint/no-unused-vars */

0 comments on commit f16db06

Please sign in to comment.