summaryrefslogtreecommitdiff
path: root/app_expo/services/api
diff options
context:
space:
mode:
Diffstat (limited to 'app_expo/services/api')
-rw-r--r--app_expo/services/api/api-config.ts27
-rw-r--r--app_expo/services/api/api-problem.test.ts100
-rw-r--r--app_expo/services/api/api-problem.ts74
-rw-r--r--app_expo/services/api/api.ts102
-rw-r--r--app_expo/services/api/api.types.ts13
-rw-r--r--app_expo/services/api/character-api.ts37
-rw-r--r--app_expo/services/api/index.ts2
7 files changed, 355 insertions, 0 deletions
diff --git a/app_expo/services/api/api-config.ts b/app_expo/services/api/api-config.ts
new file mode 100644
index 0000000..b939c0e
--- /dev/null
+++ b/app_expo/services/api/api-config.ts
@@ -0,0 +1,27 @@
+// Use this import if you want to use "env.js" file
+// const { API_URL } = require("../../config/env")
+// Or just specify it directly like this:
+const API_URL = 'http://example.com'
+
+/**
+ * The options used to configure the API.
+ */
+export interface ApiConfig {
+ /**
+ * The URL of the api.
+ */
+ url: string
+
+ /**
+ * Milliseconds before we timeout the request.
+ */
+ timeout: number
+}
+
+/**
+ * The default configuration for the app.
+ */
+export const DEFAULT_API_CONFIG: ApiConfig = {
+ url: API_URL || 'https://jsonplaceholder.typicode.com',
+ timeout: 10000,
+}
diff --git a/app_expo/services/api/api-problem.test.ts b/app_expo/services/api/api-problem.test.ts
new file mode 100644
index 0000000..f8b4041
--- /dev/null
+++ b/app_expo/services/api/api-problem.test.ts
@@ -0,0 +1,100 @@
+import { getGeneralApiProblem } from './api-problem'
+import { ApiErrorResponse } from 'apisauce'
+
+test('handles connection errors', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'CONNECTION_ERROR',
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'cannot-connect',
+ temporary: true,
+ })
+})
+
+test('handles network errors', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'NETWORK_ERROR',
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'cannot-connect',
+ temporary: true,
+ })
+})
+
+test('handles timeouts', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'TIMEOUT_ERROR',
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'timeout',
+ temporary: true,
+ })
+})
+
+test('handles server errors', () => {
+ expect(getGeneralApiProblem({ problem: 'SERVER_ERROR' } as ApiErrorResponse<null>)).toEqual({
+ kind: 'server',
+ })
+})
+
+test('handles unknown errors', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'UNKNOWN_ERROR',
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'unknown',
+ temporary: true,
+ })
+})
+
+test('handles unauthorized errors', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'CLIENT_ERROR',
+ status: 401,
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'unauthorized',
+ })
+})
+
+test('handles forbidden errors', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'CLIENT_ERROR',
+ status: 403,
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'forbidden',
+ })
+})
+
+test('handles not-found errors', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'CLIENT_ERROR',
+ status: 404,
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'not-found',
+ })
+})
+
+test('handles other client errors', () => {
+ expect(
+ getGeneralApiProblem({
+ problem: 'CLIENT_ERROR',
+ status: 418,
+ } as ApiErrorResponse<null>),
+ ).toEqual({
+ kind: 'rejected',
+ })
+})
+
+test('handles cancellation errors', () => {
+ expect(getGeneralApiProblem({ problem: 'CANCEL_ERROR' } as ApiErrorResponse<null>)).toBeNull()
+})
diff --git a/app_expo/services/api/api-problem.ts b/app_expo/services/api/api-problem.ts
new file mode 100644
index 0000000..15ca850
--- /dev/null
+++ b/app_expo/services/api/api-problem.ts
@@ -0,0 +1,74 @@
+import { ApiResponse } from 'apisauce'
+
+export type GeneralApiProblem =
+ /**
+ * Times up.
+ */
+ | { kind: 'timeout'; temporary: true }
+ /**
+ * Cannot connect to the server for some reason.
+ */
+ | { kind: 'cannot-connect'; temporary: true }
+ /**
+ * The server experienced a problem. Any 5xx error.
+ */
+ | { kind: 'server' }
+ /**
+ * We're not allowed because we haven't identified ourself. This is 401.
+ */
+ | { kind: 'unauthorized' }
+ /**
+ * We don't have access to perform that request. This is 403.
+ */
+ | { kind: 'forbidden' }
+ /**
+ * Unable to find that resource. This is a 404.
+ */
+ | { kind: 'not-found' }
+ /**
+ * All other 4xx series errors.
+ */
+ | { kind: 'rejected' }
+ /**
+ * Something truly unexpected happened. Most likely can try again. This is a catch all.
+ */
+ | { kind: 'unknown'; temporary: true }
+ /**
+ * The data we received is not in the expected format.
+ */
+ | { kind: 'bad-data' }
+
+/**
+ * Attempts to get a common cause of problems from an api response.
+ *
+ * @param response The api response.
+ */
+export function getGeneralApiProblem(response: ApiResponse<any>): GeneralApiProblem | void {
+ switch (response.problem) {
+ case 'CONNECTION_ERROR':
+ return { kind: 'cannot-connect', temporary: true }
+ case 'NETWORK_ERROR':
+ return { kind: 'cannot-connect', temporary: true }
+ case 'TIMEOUT_ERROR':
+ return { kind: 'timeout', temporary: true }
+ case 'SERVER_ERROR':
+ return { kind: 'server' }
+ case 'UNKNOWN_ERROR':
+ return { kind: 'unknown', temporary: true }
+ case 'CLIENT_ERROR':
+ switch (response.status) {
+ case 401:
+ return { kind: 'unauthorized' }
+ case 403:
+ return { kind: 'forbidden' }
+ case 404:
+ return { kind: 'not-found' }
+ default:
+ return { kind: 'rejected' }
+ }
+ case 'CANCEL_ERROR':
+ return null
+ }
+
+ return null
+}
diff --git a/app_expo/services/api/api.ts b/app_expo/services/api/api.ts
new file mode 100644
index 0000000..4093d34
--- /dev/null
+++ b/app_expo/services/api/api.ts
@@ -0,0 +1,102 @@
+import { ApisauceInstance, create, ApiResponse } from 'apisauce'
+import { getGeneralApiProblem } from './api-problem'
+import { ApiConfig, DEFAULT_API_CONFIG } from './api-config'
+import * as Types from './api.types'
+
+/**
+ * Manages all requests to the API.
+ */
+export class Api {
+ /**
+ * The underlying apisauce instance which performs the requests.
+ */
+ apisauce: ApisauceInstance
+
+ /**
+ * Configurable options.
+ */
+ config: ApiConfig
+
+ /**
+ * Creates the api.
+ *
+ * @param config The configuration to use.
+ */
+ constructor(config: ApiConfig = DEFAULT_API_CONFIG) {
+ this.config = config
+ }
+
+ /**
+ * Sets up the API. This will be called during the bootup
+ * sequence and will happen before the first React component
+ * is mounted.
+ *
+ * Be as quick as possible in here.
+ */
+ setup() {
+ // construct the apisauce instance
+ this.apisauce = create({
+ baseURL: this.config.url,
+ timeout: this.config.timeout,
+ headers: {
+ Accept: 'application/json',
+ },
+ })
+ }
+
+ /**
+ * Gets a list of users.
+ */
+ async getUsers(): Promise<Types.GetUsersResult> {
+ // make the api call
+ const response: ApiResponse<any> = await this.apisauce.get(`/users`)
+
+ // the typical ways to die when calling an api
+ if (!response.ok) {
+ const problem = getGeneralApiProblem(response)
+ if (problem) return problem
+ }
+
+ const convertUser = (raw) => {
+ return {
+ id: raw.id,
+ name: raw.name,
+ }
+ }
+
+ // transform the data into the format we are expecting
+ try {
+ const rawUsers = response.data
+ const resultUsers: Types.User[] = rawUsers.map(convertUser)
+ return { kind: 'ok', users: resultUsers }
+ } catch {
+ return { kind: 'bad-data' }
+ }
+ }
+
+ /**
+ * Gets a single user by ID
+ */
+
+ async getUser(id: string): Promise<Types.GetUserResult> {
+ // make the api call
+ const response: ApiResponse<any> = await this.apisauce.get(`/users/${id}`)
+
+ // the typical ways to die when calling an api
+ if (!response.ok) {
+ const problem = getGeneralApiProblem(response)
+ if (problem) return problem
+ }
+
+ // transform the data into the format we are expecting
+ try {
+ const resultUser: Types.User = {
+ id: response.data.id,
+ name: response.data.name,
+ }
+ return { kind: 'ok', user: resultUser }
+ } catch {
+ return { kind: 'bad-data' }
+ }
+ }
+}
diff --git a/app_expo/services/api/api.types.ts b/app_expo/services/api/api.types.ts
new file mode 100644
index 0000000..21d3c87
--- /dev/null
+++ b/app_expo/services/api/api.types.ts
@@ -0,0 +1,13 @@
+import { GeneralApiProblem } from './api-problem'
+import { Character } from '../../models/character/character'
+
+export interface User {
+ id: number
+ name: string
+}
+
+export type GetUsersResult = { kind: 'ok'; users: User[] } | GeneralApiProblem
+export type GetUserResult = { kind: 'ok'; user: User } | GeneralApiProblem
+
+export type GetCharactersResult = { kind: 'ok'; characters: Character[] } | GeneralApiProblem
+export type GetCharacterResult = { kind: 'ok'; character: Character } | GeneralApiProblem
diff --git a/app_expo/services/api/character-api.ts b/app_expo/services/api/character-api.ts
new file mode 100644
index 0000000..e28ec48
--- /dev/null
+++ b/app_expo/services/api/character-api.ts
@@ -0,0 +1,37 @@
+import { ApiResponse } from 'apisauce'
+import { Api } from './api'
+import { GetCharactersResult } from './api.types'
+import { getGeneralApiProblem } from './api-problem'
+
+const API_PAGE_SIZE = 50
+
+export class CharacterApi {
+ private api: Api
+
+ constructor(api: Api) {
+ this.api = api
+ }
+
+ async getCharacters(): Promise<GetCharactersResult> {
+ try {
+ // make the api call
+ const response: ApiResponse<any> = await this.api.apisauce.get(
+ 'https://raw.githubusercontent.com/infinitered/ignite/master/data/rick-and-morty.json',
+ { amount: API_PAGE_SIZE },
+ )
+
+ // the typical ways to die when calling an api
+ if (!response.ok) {
+ const problem = getGeneralApiProblem(response)
+ if (problem) return problem
+ }
+
+ const characters = response.data.results
+
+ return { kind: 'ok', characters }
+ } catch (e) {
+ __DEV__ && console.tron.log(e.message)
+ return { kind: 'bad-data' }
+ }
+ }
+}
diff --git a/app_expo/services/api/index.ts b/app_expo/services/api/index.ts
new file mode 100644
index 0000000..f133e64
--- /dev/null
+++ b/app_expo/services/api/index.ts
@@ -0,0 +1,2 @@
+export * from './api'
+export * from './api.types'