diff --git a/src/clients/LdbClientV2.ts b/src/clients/LdbClientV2.ts new file mode 100644 index 0000000..84c65f0 --- /dev/null +++ b/src/clients/LdbClientV2.ts @@ -0,0 +1,9 @@ +import { BaseOwlBoardClient } from "./client"; + +export class LdbClientV2 { + private client: BaseOwlBoardClient; + + constructor(client: BaseOwlBoardClient) { + this.client = client; + } +} \ No newline at end of file diff --git a/src/clients/LocationReferenceClientV2.ts b/src/clients/LocationReferenceClientV2.ts new file mode 100644 index 0000000..920ad1d --- /dev/null +++ b/src/clients/LocationReferenceClientV2.ts @@ -0,0 +1,9 @@ +import { BaseOwlBoardClient } from "./client"; + +export class LocationReferenceClientV2 { + private client: BaseOwlBoardClient; + + constructor(client: BaseOwlBoardClient) { + this.client = client; + } +} \ No newline at end of file diff --git a/src/clients/MiscClientV2.ts b/src/clients/MiscClientV2.ts new file mode 100644 index 0000000..f6bad1a --- /dev/null +++ b/src/clients/MiscClientV2.ts @@ -0,0 +1,9 @@ +import { BaseOwlBoardClient } from "./client"; + +export class MiscClientV2 { + private client: BaseOwlBoardClient; + + constructor(client: BaseOwlBoardClient) { + this.client = client; + } +} \ No newline at end of file diff --git a/src/clients/PisClientV2.ts b/src/clients/PisClientV2.ts new file mode 100644 index 0000000..17bd010 --- /dev/null +++ b/src/clients/PisClientV2.ts @@ -0,0 +1,19 @@ +import { BaseOwlBoardClient } from "./client"; +import { ValidationError } from "../errors"; + +export class PisClientV2 { + private client: BaseOwlBoardClient; + + constructor(client: BaseOwlBoardClient) { + this.client = client; + } + + async getStopsByPis(code: string): Promise { + const codeRegex = /^\d{4}$/; + if (!codeRegex.test(code)) { + throw new ValidationError("Invalid input: code must be a four-character string consisting of only numerals") + } + const path = `/api/v2/pis/byCode/${code}` + return this.client.makeRequest("GET", path); + } +} \ No newline at end of file diff --git a/src/clients/TrainClientV2.ts b/src/clients/TrainClientV2.ts new file mode 100644 index 0000000..c21d398 --- /dev/null +++ b/src/clients/TrainClientV2.ts @@ -0,0 +1,9 @@ +import { BaseOwlBoardClient } from "./client"; + +export class TrainClientV2 { + private client: BaseOwlBoardClient; + + constructor(client: BaseOwlBoardClient) { + this.client = client; + } +} \ No newline at end of file diff --git a/src/clients/UserClientV2.ts b/src/clients/UserClientV2.ts new file mode 100644 index 0000000..9f08e01 --- /dev/null +++ b/src/clients/UserClientV2.ts @@ -0,0 +1,9 @@ +import { BaseOwlBoardClient } from "./client"; + +export class UserClientV2 { + private client: BaseOwlBoardClient; + + constructor(client: BaseOwlBoardClient) { + this.client = client; + } +} \ No newline at end of file diff --git a/src/clients/client.ts b/src/clients/client.ts new file mode 100644 index 0000000..e8690f4 --- /dev/null +++ b/src/clients/client.ts @@ -0,0 +1,96 @@ +import { version } from "../constants"; +import { LdbClientV2 } from "./LdbClientV2"; +import { LocationReferenceClientV2 } from "./LocationReferenceClientV2"; +import { MiscClientV2 } from "./MiscClientV2"; +import { PisClientV2 } from "./PisClientV2"; +import { TrainClientV2 } from "./TrainClientV2"; +import { UserClientV2 } from "./UserClientV2"; + +import { ApiError, NetworkError } from "../errors"; + +export class BaseOwlBoardClient { + protected baseUrl: string; + protected apiKey: string; + protected headers: Record; + + constructor(baseUrl: string, apiKey: string) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.headers = { + "Content-Type": "application/json", + "Accept": "application/json", + "User-Agent": `OwlBoardTS/v${version}`, + "uuid": this.apiKey, + }; + } + + async makeRequest(method: string, path: string, body?: any): Promise { + const url = `${this.baseUrl}${path}`; + const options: RequestInit = { + method, + headers: this.headers, + body: body ? JSON.stringify(body) : undefined, + }; + + let attempt = 0; + const retries = 3; + const delay = 500; + const retryableStatusCodes = [408, 500, 502, 503, 504]; + + while (attempt < retries) { + try { + const response = await fetch(url, options); + if (!response.ok) { + const responseBody = await response.text(); + const apiError = new ApiError("API Request Failed", response.status, responseBody); + if (retryableStatusCodes.includes(response.status)) { + attempt++; + if (attempt >= retries) { // Max attempts reached + throw apiError; + } + // Wait before retrying + await new Promise(res => setTimeout(res, delay)); + continue; // Retry + } else { // Non-retryable status codes + throw apiError; + } + } + + return (await response.json()) as T; + } catch (error) { + if (error instanceof TypeError) { // Indicates network errors with fetch + if (attempt < retries) { + attempt ++; + await new Promise(res => setTimeout(res, delay)); + continue; + } + // Throw network error if max tries reached handling TypeError + throw new NetworkError("Network request failed"); + }; + + throw error; // Rethrow any other error + }; + } + // This code should never be reached. + throw new Error("Unexpected error after multiple retries.") + } +} + +export class OwlBoardClientV2 extends BaseOwlBoardClient { + pis: PisClientV2; + train: TrainClientV2; + user: UserClientV2; + locationReference: LocationReferenceClientV2; + misc: MiscClientV2; + ldb: LdbClientV2; + + constructor(baseUrl: string, apiKey: string) { + super(baseUrl, apiKey); + this.pis = new PisClientV2(this); + this.train = new TrainClientV2(this); + this.user = new UserClientV2(this); + this.locationReference = new LocationReferenceClientV2(this); + this.misc = new MiscClientV2(this); + this.ldb = new LdbClientV2(this); + } +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts new file mode 100644 index 0000000..2887449 --- /dev/null +++ b/src/constants.ts @@ -0,0 +1 @@ +export const version: string = "0.0.1" \ No newline at end of file diff --git a/src/errors.ts b/src/errors.ts new file mode 100644 index 0000000..47d5781 --- /dev/null +++ b/src/errors.ts @@ -0,0 +1,28 @@ +export class ApiError extends Error { + statusCode: number; + responseBody: string; + + constructor(message: string, statusCode: number, responseBody: string) { + super(message); + this.name = 'ApiError'; + this.statusCode = statusCode; + this.responseBody = responseBody; + Object.setPrototypeOf(this, ApiError.prototype); + } +} + +export class ValidationError extends Error { + constructor(message: string) { + super(message); + this.name = 'ValidationError'; + Object.setPrototypeOf(this, ValidationError.prototype); + } +} + +export class NetworkError extends Error { + constructor(message: string) { + super(message); + this.name = 'NetworkError'; + Object.setPrototypeOf(this, NetworkError.prototype); + } +} diff --git a/src/index.ts b/src/index.ts index 9c7b939..6d6c401 100644 --- a/src/index.ts +++ b/src/index.ts @@ -15,3 +15,5 @@ OwlBoardTS: An OwlBoard API Client written in TypeScript You should have received a copy of the GNU General Public License along with this program. If not, see . */ + +export { OwlBoardClientV2 } from './clients/client' \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index d9e1e0f..ac86154 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -107,6 +107,6 @@ // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ "skipLibCheck": true /* Skip type checking all .d.ts files. */ }, - "include": ["src/**/*"], + "include": ["src/**/*.ts"], "exclude": ["node_modules"], }