Skip to content

Instantly share code, notes, and snippets.

@NWYLZW
Created December 27, 2021 06:42
Show Gist options
  • Save NWYLZW/ec18dc7cee3e14812ad3f5029c31b276 to your computer and use it in GitHub Desktop.
Save NWYLZW/ec18dc7cee3e14812ad3f5029c31b276 to your computer and use it in GitHub Desktop.
Rester
import MockAdapter from 'axios-mock-adapter'
import { AxiosInstance } from 'axios'
import { expect, use } from 'chai'
import cap from 'chai-as-promised'
import { Api, attachApi } from '../src/api'
use(cap)
describe('Api', function () {
interface Foo {
get guilds(): Promise<any[]> & {
add(nGuild: string): Promise<string>
}
guild(id: string): Promise<any> & {
upd(nGuild: string): Promise<string>
del(): Promise<string>
get members(): Promise<any[]>
member(id: string): Promise<any>
}
}
class Foo extends Api {
constructor() {
super('http://www.example.com')
return attachApi(this)
}
}
const foo = new Foo()
it('should travel simple property and simple method.', async () => {
const id = '123'
new MockAdapter(foo.$request as AxiosInstance)
.onGet('/guilds')
.replyOnce(200, 'test0')
.onGet(`/guilds/${ id }`)
.replyOnce(200, 'test1')
expect(await foo.guilds)
.to.equal('test0')
expect(await foo.guild(id))
.to.equal('test1')
})
it('should travel nest property and nest method.', async () => {
const guildId = '123', memberId = '456'
new MockAdapter(foo.$request as AxiosInstance)
.onGet(`/guilds/${ guildId }/members`)
.replyOnce(200, 'test0')
.onGet(`/guilds/${ guildId }/members/${ memberId }`)
.replyOnce(200, 'test1')
expect(await foo.guild(guildId).members)
.to.equal('test0')
expect(await foo.guild(guildId).member(memberId))
.to.equal('test1')
})
it('should travel other method request.', async () => {
const reqAdd = 'content'
const guildId = '123'
new MockAdapter(foo.$request as AxiosInstance)
.onPost('/guilds', reqAdd)
.replyOnce(200, 'test0')
.onPatch(`/guilds/${ guildId }`, reqAdd)
.replyOnce(200, 'test1')
.onDelete(`/guilds/${ guildId }`)
.replyOnce(200, 'test2')
expect(await foo.guilds.add(reqAdd))
.to.equal('test0')
expect(await foo.guild(guildId).upd(reqAdd))
.to.equal('test1')
expect(await foo.guild(guildId).del())
.to.equal('test2')
})
it('should listen `resp.fulfilled` and `resp.rejected` event.', async () => {
new MockAdapter(foo.$request as AxiosInstance)
.onGet('/guilds')
.replyOnce(200, 'test0')
.onGet('/guilds')
.replyOnce(403, 'test1')
.onGet('/guilds')
.replyOnce(404, 'test2')
foo.on('resp.fulfilled', resp => {
return resp.data + '-event'
})
foo.on('resp.rejected', error => {
switch (error.response?.status) {
case 403:
return error.response.data + '-403event'
case 404:
console.log(error.response.data)
throw new Error(error.response.data + '-404event')
}
})
expect(await foo.guilds)
.to.equal('test0-event')
expect(await foo.guilds)
.to.equal('test1-403event')
await expect(foo.guilds)
.to.eventually.rejectedWith('test2-404event')
await foo.guilds.catch(error => {
expect(error.message).to.equal('test2-404event')
})
})
})
import { Utils } from './utils'
import axios, { AxiosResponse, AxiosError, AxiosInstance, AxiosRequestConfig } from 'axios'
type TwoParamsMethod = 'get' | 'delete' | 'head' | 'options'
type ThreeParamsMethod = 'post' | 'put' | 'patch'
interface TwoParamsRequest {
<T, D = any>(url: string, config?: AxiosRequestConfig<D>): Promise<T>
}
interface ThreeParamsRequest {
<T, D = any>(url: string, data?: D, config?: AxiosRequestConfig<D>): Promise<T>
}
export type InnerAxiosInstance = Omit<AxiosInstance, 'request' | TwoParamsMethod | ThreeParamsMethod> & {
request<T = any, D = any>(config: AxiosRequestConfig<D>): Promise<T>
} & {
[K in TwoParamsMethod]: TwoParamsRequest
} & {
[K in ThreeParamsMethod]: ThreeParamsRequest
}
type promiseMethod = 'then' | 'catch' | 'finally'
const promiseMethods = [ 'then', 'catch', 'finally' ]
const getPromiseProp = (p: Promise<any>, prop: promiseMethod) => p[prop].bind(p)
const requestProxy = (a: Api, path: string, cPath = ''): Function => new Proxy(() => {}, {
get(_, prop: string) {
if (promiseMethods.includes(prop))
return getPromiseProp(
a.$request.get(path + cPath), prop as promiseMethod)
else {
switch (prop as 'add' | 'del' | 'upd') {
case 'add':
return (d: any) => a.$request.post(path + cPath, d)
case 'del':
return () => a.$request.delete(path + cPath)
case 'upd':
return (d: any) => a.$request.patch(path + cPath, d)
}
return requestProxy(a, path, `/${prop}`)
}
},
apply(_, __, [id, ..._args]) {
return requestProxy(a, `${path + Utils.String.pluralize(cPath)}/${id}`)
}
})
export const attachApi = <T extends Api>(a: T) => new Proxy(a, {
get(target, path: keyof Api) {
if (path in target) return target[path]
return requestProxy(a, `/${path}`)
}
})
export class Api {
readonly host: string
readonly events = <{
[K in keyof Api.EventMap]: Api.EventMap[K]
}>{}
$request: InnerAxiosInstance
constructor(host: string) {
this.host = host
this.$request = this.getRequest()
}
on<E extends keyof Api.EventMap>(event: E, cb: Api.EventMap[E]) {
this.events[event] = cb
}
emit<E extends keyof Api.EventMap>(event: E, ...args: Parameters<Api.EventMap[E]>) {
if (event in this.events)
// @ts-ignore
return this.events[event](...args)
switch (event) {
case 'resp.fulfilled':
return (<AxiosResponse>args[0]).data
case 'resp.rejected':
throw args[0]
default:
return
}
}
protected getRequest() {
const a = axios.create({
baseURL: this.host,
headers: {
'Content-Type': 'application/json'
}
})
// @ts-ignore
a.interceptors.response.use(this.emit.bind(this, 'resp.fulfilled'), this.emit.bind(this, 'resp.rejected'))
return a
}
}
export namespace Api {
export interface EventMap {
'resp.fulfilled': (resp: AxiosResponse) => void
'resp.rejected': (error: AxiosError<{}>) => void
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment