Skip to content

Commit

Permalink
refactor(client): move to generics based client by leveraging consist…
Browse files Browse the repository at this point in the history
…ent factory interface 🏭

This serves to significantly reduce the amount of code that makes up our client.
  • Loading branch information
ReidWeb committed Feb 6, 2022
1 parent 65601b8 commit a50af60
Show file tree
Hide file tree
Showing 16 changed files with 273 additions and 181 deletions.
28 changes: 23 additions & 5 deletions src/client/LodestoneClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import Axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios'
import Cheerio, { CheerioAPI } from 'cheerio'
import pLimit, { Limit } from 'p-limit'
import Character from '../entity/character/Character'
import IClientProps from './interface/IClientProps'
import Language from '../locale/Language'
import LocalizedClientFactory from '../locale/LocalizedClientFactory'
Expand All @@ -41,8 +40,11 @@ import Response from './Response'
import RequestStatus from './category/RequestStatus'
import LodestoneError from './error/LodestoneError'
import RequestFailureCategory from './category/RequestFailureCategory'
import ParsingError from './error/ParsingError'
import IFactory from '../parser/IFactory'
import ParsableEntity from '../parser/ParsableEntity'

export type OnSuccessFunction = (id: number, character?: Character) => void
export type OnSuccessFunction<IdentifierType, TypeOfValue> = (id: IdentifierType, value?: TypeOfValue) => void
export type OnErrorFunction = (id: number, error: Error) => void
export type GetEntityFunction<IdentifierType, TypeOfValue> = (
id: IdentifierType,
Expand All @@ -56,16 +58,24 @@ export type GetEntityFunction<IdentifierType, TypeOfValue> = (
* @param language
* @param targetFunction
*/
export default abstract class LodestoneClient<IdentifierType, TypeOfValue> implements IClientProps {
export default abstract class LodestoneClient<
IdentifierType,
TypeOfInterface,
TypeOfValue extends ParsableEntity<IdentifierType, TypeOfInterface>
> implements IClientProps
{
cheerioInstance: CheerioAPI

axiosInstances?: OptionalPerLanguageMapping<AxiosInstance>

factory: IFactory<IdentifierType, TypeOfInterface, TypeOfValue>

parallelismLimit: Limit

public defaultLanguage: Language

public constructor(props?: IClientProps) {
public constructor(factory: IFactory<IdentifierType, TypeOfInterface, TypeOfValue>, props?: IClientProps) {
this.factory = factory
this.defaultLanguage = props?.defaultLanguage || Language.en
this.cheerioInstance = props?.cheerioInstance || Cheerio
this.parallelismLimit = props?.parallelismLimit || pLimit(5)
Expand Down Expand Up @@ -125,7 +135,15 @@ export default abstract class LodestoneClient<IdentifierType, TypeOfValue> imple
}
}

protected abstract get(id: IdentifierType, language?: Language): Promise<TypeOfValue>
async get(id: IdentifierType, language?: Language): Promise<TypeOfValue> {
const path = this.factory.getUrlForId(id)
const response = await this.getPath(this.factory.returnType, path, id, language)
try {
return this.factory.fromPage(id, response, this.cheerioInstance, language || this.defaultLanguage)
} catch (e) {
throw new ParsingError(this.factory.returnType, path, id, <Error>e)
}
}

public async getAsResponse(id: IdentifierType, language?: Language): Promise<Response<IdentifierType, TypeOfValue>> {
try {
Expand Down
94 changes: 72 additions & 22 deletions src/client/__tests__/LodestoneClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,32 +25,82 @@

import LodestoneClient from '../LodestoneClient'
import { AxiosError, AxiosResponse } from 'axios'
import language from '../../locale/Language'
import { LocalizedClientFactory } from '../../locale'
import Language from '../../locale/Language'
import UninitialisedClientError from '../error/UninitialisedClientError'
import LodestoneMaintenanceError from '../error/LodestoneMaintenanceError'
import TooManyRequestsError from '../error/TooManyRequestsError'
import PageNotFoundError from '../error/PageNotFoundError'
import LodestoneError from '../error/LodestoneError'
import RequestTimedOutError from '../error/RequestTimedOutError'
import UnknownError from '../error/UnknownError'
import Character from '../../entity/character/Character'
import RequestStatus from '../category/RequestStatus'
import Response from '../Response'
import { ISuccessResponse } from '../interface/IResponse'
import ParsableEntity from '../../parser/ParsableEntity'
import { CheerioAPI } from 'cheerio'
import IFactory from '../../parser/IFactory'
import IClientProps from '../interface/IClientProps'

interface IDummy {
mutated?: boolean
// eslint-disable-next-line @typescript-eslint/no-explicit-any
resp?: AxiosResponse<string, any> | undefined
}

class Dummy extends ParsableEntity<number, IDummy> implements IDummy {
mutated?: boolean

// eslint-disable-next-line @typescript-eslint/no-explicit-any
resp?: AxiosResponse<string, any> | undefined

initializeFromPage(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
data: string,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
cheerio: CheerioAPI,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
language: Language,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
config?: {
foo: boolean
}
) {
this.mutated = true
}
}

class DummyFactory implements IFactory<number, IDummy, Dummy> {
readonly returnType: string

interface DummyResult {
mutated: boolean
resp: AxiosResponse<string>
constructor() {
this.returnType = 'TestCharacter'
}

getUrlForId(id: number): string {
return `character/${id}`
}

public fromPage(
id: number,
response: AxiosResponse<string>,
cheerio: CheerioAPI,
language: Language,
config?: {
foo: boolean
}
): Dummy {
const instance = new Dummy(id)
instance.initializeFromPage(response.data, cheerio, language, config)
instance.resp = response
return instance
}
}

class TestLodestoneClient extends LodestoneClient<number, DummyResult> {
async get(id: number, languageToUse?: Language): Promise<DummyResult> {
return { mutated: true, resp: await this.getPath('TestCharacter', `character/${id}`, id, languageToUse) }
class TestLodestoneClient extends LodestoneClient<number, IDummy, Dummy> {
constructor(props: IClientProps) {
super(new DummyFactory(), props)
}
}

// eslint-disable-next-line jsdoc/require-returns
/**
* @param code
*/
Expand Down Expand Up @@ -80,18 +130,18 @@ describe('LodestoneClient', () => {
}

let client: TestLodestoneClient
const overriddenDefaultLanguage = language.fr
const overriddenDefaultLanguage = Language.fr

beforeAll(() => {
client = new TestLodestoneClient({
defaultLanguage: overriddenDefaultLanguage,
axiosInstances: LocalizedClientFactory.createClientsForLanguages([language.en, language.fr]),
axiosInstances: LocalizedClientFactory.createClientsForLanguages([Language.en, Language.fr]),
})
})

describe('when a caller tries to specify a language that is not initialized', () => {
it('should reject with uninitialised client error', async () => {
await expect(client.get(11886902, language.ja)).rejects.toThrow(UninitialisedClientError)
await expect(client.get(11886902, Language.ja)).rejects.toThrow(UninitialisedClientError)
})
})

Expand Down Expand Up @@ -122,7 +172,7 @@ describe('LodestoneClient', () => {
// @ts-ignore
client.axiosInstances[overriddenDefaultLanguage].get = jest.fn().mockResolvedValue(goodResp)
// @ts-ignore
const spy = (client.axiosInstances[language.en].get = jest.fn().mockResolvedValue(goodResp))
const spy = (client.axiosInstances[Language.en].get = jest.fn().mockResolvedValue(goodResp))
await client.get(11886902)
expect(spy).toHaveBeenCalledTimes(0)
})
Expand All @@ -135,8 +185,8 @@ describe('LodestoneClient', () => {
// @ts-ignore
defaultLangSpy = client.axiosInstances[overriddenDefaultLanguage].get = jest.fn().mockResolvedValue(goodResp)
// @ts-ignore
specifiedLangSpy = client.axiosInstances[language.en].get = jest.fn().mockResolvedValue(goodResp)
await client.get(11886902, language.en)
specifiedLangSpy = client.axiosInstances[Language.en].get = jest.fn().mockResolvedValue(goodResp)
await client.get(11886902, Language.en)
})

it('should call the client for the target language', () => {
Expand All @@ -149,7 +199,7 @@ describe('LodestoneClient', () => {
})

describe('when a response is returned from the call', () => {
let resp: AxiosResponse
let resp: AxiosResponse | undefined
beforeAll(async () => {
// @ts-ignore
client.axiosInstances[overriddenDefaultLanguage].get = jest.fn().mockResolvedValue(goodResp)
Expand All @@ -158,11 +208,11 @@ describe('LodestoneClient', () => {
})

it('should return the response status upstream', () => {
expect(resp.status).toEqual(goodResp.status)
expect(resp?.status).toEqual(goodResp.status)
})

it('should return the response data upstream', () => {
expect(resp.data).toEqual(goodResp.data)
expect(resp?.data).toEqual(goodResp.data)
})
})
describe('when a response is returned but HTTP status code is', () => {
Expand Down Expand Up @@ -265,12 +315,12 @@ describe('LodestoneClient', () => {
})

describe('when getting a provided path is being fetched as a response', () => {
let result: ISuccessResponse<number, DummyResult>
let result: ISuccessResponse<number, IDummy>

beforeAll(async () => {
// @ts-ignore
client.axiosInstances[overriddenDefaultLanguage].get = jest.fn().mockResolvedValue('Hello')
result = (await client.getAsResponse(1)) as ISuccessResponse<number, DummyResult>
result = (await client.getAsResponse(1)) as ISuccessResponse<number, IDummy>
})

it("should yield the result of the derived class' get function on value key", () => {
Expand Down
14 changes: 5 additions & 9 deletions src/client/entity/CharacterClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,12 @@ import Character from '../../entity/character/Character'
import Language from '../../locale/Language'
import ParsingError from '../error/ParsingError'
import ICharacter from '../../entity/character/interface/ICharacter'
import CharacterFactory from '../../entity/character/CharacterFactory'
import IClientProps from '../interface/IClientProps'

export default class CharacterClient extends LodestoneClient<number, ICharacter> {
public async get(id: number, language?: Language): Promise<Character> {
const path = `/character/${id}`
const response = await this.getPath('Character', path, id, language)
try {
return Character.fromPage(id, response.data, this.cheerioInstance, language || this.defaultLanguage)
} catch (e) {
throw new ParsingError('Character', path, id, <Error>e)
}
export default class CharacterClient extends LodestoneClient<number, ICharacter, Character> {
constructor(props?: IClientProps) {
super(new CharacterFactory(), props)
}

// public async getCharacters(
Expand Down
4 changes: 2 additions & 2 deletions src/client/entity/__itests__/CharacterClient.itest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@
*
*/

import CharacterNotFoundError from '../../../errors/CharacterNotFoundError'
import CharacterClient from '../CharacterClient'
import PageNotFoundError from '../../error/PageNotFoundError'

describe('Character Client [Integration]', () => {
let client: CharacterClient
Expand All @@ -37,7 +37,7 @@ describe('Character Client [Integration]', () => {
describe('when the character does not exist', () => {
jest.setTimeout(100000)
it('should throw a character not found error', async () => {
await expect(client.get(11886905)).rejects.toThrow(CharacterNotFoundError)
await expect(client.get(11886905)).rejects.toThrow(PageNotFoundError)
})
})
})
Expand Down
4 changes: 2 additions & 2 deletions src/client/entity/__tests__/CharacterClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@
*/

import Axios from 'axios'
import CharacterNotFoundError from '../../../errors/CharacterNotFoundError'
import CharacterClient from '../CharacterClient'
import language from '../../../locale/Language'
import PageNotFoundError from '../../error/PageNotFoundError'

describe('CharacterClient Client', () => {
describe('when fetching a character by id', () => {
Expand All @@ -50,7 +50,7 @@ describe('CharacterClient Client', () => {
})

it('should throw a character not found error', async () => {
await expect(localClient.get(11886905)).rejects.toThrow(CharacterNotFoundError)
await expect(localClient.get(11886905)).rejects.toThrow(PageNotFoundError)
})
})
})
Expand Down
1 change: 0 additions & 1 deletion src/client/error/TooManyRequestsError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import LodestoneError from './LodestoneError'
import RequestStatus from '../category/RequestStatus'
import RequestFailureCategory from '../category/RequestFailureCategory'
import { FailureResponse } from '../Response'
import { IRejectedRequestFailure } from '../interface/IResponse'

export default class TooManyRequestsError<TypeOfIdentifier> extends LodestoneError<TypeOfIdentifier> {
Expand Down
Loading

0 comments on commit a50af60

Please sign in to comment.