Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improvement: validate response #2451

Merged
merged 2 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
103 changes: 103 additions & 0 deletions src/utils/validate.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/* 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>

let complexResponseExample: any
if (ValidateComplexResponseExample(complexResponseExample)) {
// `complexResponseExample` has been generated type now. You can try:
type Wallets = typeof complexResponseExample.data.wallets
const balance: ExtractedBalances | null | undefined = complexResponseExample.data.wallets[0].balances
} else {
// complexResponseExample is unknown type
complexResponseExample
}
/* eslint-enable @typescript-eslint/no-unused-vars */
Loading