Skip to content

Commit

Permalink
feat: support API v4 endpoint /version
Browse files Browse the repository at this point in the history
closes #24
  • Loading branch information
Daniel J. Lauk committed Nov 16, 2021
2 parents 6b003d6 + 3ac978c commit 383442d
Show file tree
Hide file tree
Showing 6 changed files with 187 additions and 16 deletions.
38 changes: 34 additions & 4 deletions src/api/util.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,45 @@ describe('module api/util', () => {
})

describe('detectVersion', () => {
it('returns version string from version field in response', async () => {
const version = '1.2.3-rc4'
it('returns version string for API v4 with major and minor', async () => {
const expectedVersion = '4.2'
mockedFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: () => Promise.resolve({ version }),
json: () =>
Promise.resolve({ data_api_version: { major: 4, minor: 2 } }),
} as Response)
const actualVersion = await detectVersion(DEFAULT_URL)
expect(actualVersion).toBe(version)
expect(actualVersion).toBe(expectedVersion)
})

it('returns version string for API v4 with major version only', async () => {
const expectedVersion = '4'
mockedFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: () => Promise.resolve({ data_api_version: { major: 4 } }),
} as Response)
const actualVersion = await detectVersion(DEFAULT_URL)
expect(actualVersion).toBe(expectedVersion)
})

it('throws if unsupported response format', async () => {
mockedFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: () =>
Promise.resolve({
new_version_structure: {
major: 5,
minor: 2,
patch: 7,
text: '5.2.7',
build: 'experimental',
},
}),
} as Response)
await expect(detectVersion(DEFAULT_URL)).rejects.toThrow()
})

it('returns undefined on HTTP 404', async () => {
Expand Down
25 changes: 13 additions & 12 deletions src/api/util.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,5 @@
import * as decoders from 'decoders'
import { fetchWithTimeout } from '../http'

type VersionInfo = {
version: string
}
const versionGuard = decoders.guard(
decoders.object({ version: decoders.string })
)
import { dataApiVersionResponseGuard } from './v4/apiv4decoders'

/**
* Contact the API provider at base URL `url` and try to read
Expand All @@ -24,17 +17,25 @@ export const detectVersion = async (
headers: { Accept: 'application/json' },
})
if (resp.status === 404) {
return undefined
return undefined // OK, this API provider doesn't provide a /version endpoint
}
if (!resp.ok) {
throw new Error(
`could not detect version: ${resp.status} ${resp.statusText}`
)
}
const respData: unknown = await resp.json()
// in turn, try each known API version
try {
const versionInfo: VersionInfo = versionGuard(await resp.json())
return versionInfo.version
const tmp = dataApiVersionResponseGuard(respData)
let v = `${tmp.data_api_version.major}`
if (tmp.data_api_version.minor) {
v += `.${tmp.data_api_version.minor}`
}
return v
} catch (e) {
throw new Error('received malformed version info')
// decoder throws error --> it's not Data API V4; try next one (if there is any)
}
// out of known API versions / providers --> throw error
throw new Error('could not identify version from API response')
}
37 changes: 37 additions & 0 deletions src/api/v4/apiv4.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
DataApiV4ChannelSearchOptions,
DataApiV4ChannelSearchResult,
DataApiV4Client,
DataApiV4DataApiVersionResult,
DataApiV4EventsQueryOptions,
DataApiV4EventsQueryResult,
} from './apiv4'
Expand Down Expand Up @@ -225,4 +226,40 @@ describe('class DataApiV4Client', () => {
expect(result).toEqual(DUMMY_RESPONSE)
})
})

describe('method queryDataApiVersion', () => {
const DUMMY_RESPONSE: DataApiV4DataApiVersionResult = {
data_api_version: {
major: 4,
minor: 5,
},
}

beforeEach(() => {
mockedGet.mockResolvedValue(DUMMY_RESPONSE)
})

it('sends a GET request to the right URL', async () => {
const expectedUrl = `${BASE_URL}/version`
await api.queryDataApiVersion()
expect(mockedGet).toHaveBeenCalledTimes(1)
const actualUrl = mockedGet.mock.calls[0][0] as string
expect(actualUrl).toEqual(expectedUrl)
})

it('uses DEFAULT_TIMEOUT if not specified', async () => {
await api.queryDataApiVersion()
expect(mockedGet.mock.calls[0][1]).toBe(DEFAULT_TIMEOUT)
})

it('overrides DEFAULT_TIMEOUT if specified', async () => {
await api.queryDataApiVersion(60000)
expect(mockedGet.mock.calls[0][1]).toBe(60000)
})

it('returns version events', async () => {
const result = await api.queryDataApiVersion()
expect(result).toEqual(DUMMY_RESPONSE)
})
})
})
18 changes: 18 additions & 0 deletions src/api/v4/apiv4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
backendsResponseGuard,
binnedQueryResponseGuard,
channelSearchResponseGuard,
dataApiVersionResponseGuard,
eventsQueryResponseGuard,
} from './apiv4decoders'

Expand Down Expand Up @@ -50,6 +51,14 @@ export type DataApiV4ChannelSearchResult = {
channels: DataApiV4ChannelSearchResultItem[]
}

/** response for a data api version query operation */
export type DataApiV4DataApiVersionResult = {
data_api_version: {
major: number
minor?: number
}
}

/** options for an events query operation */
export type DataApiV4EventsQueryOptions = {
/** the backend where the channel is stored */
Expand Down Expand Up @@ -169,6 +178,15 @@ export class DataApiV4Client {
return backendsResponseGuard(result)
}

/** query data api version */
public async queryDataApiVersion(
timeoutMs: number = DEFAULT_TIMEOUT
): Promise<DataApiV4DataApiVersionResult> {
const url = `${this.baseUrl}/version`
const result = await get(url, timeoutMs)
return dataApiVersionResponseGuard(result)
}

/** query for data (raw) */
public async queryEvents(
queryOptions: DataApiV4EventsQueryOptions,
Expand Down
76 changes: 76 additions & 0 deletions src/api/v4/apiv4decoders.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {
binnedQueryResponseGuard,
channelSearchResponseGuard,
eventsQueryResponseGuard,
dataApiVersionResponseGuard,
} from './apiv4decoders'

describe('module apiv4decoders', () => {
Expand Down Expand Up @@ -449,4 +450,79 @@ describe('module apiv4decoders', () => {
expect(() => binnedQueryResponseGuard(input)).toThrowError()
})
})

describe('dataApiVersionResponseGuard', () => {
it('works with good data', () => {
const input = {
data_api_version: {
major: 4,
minor: 0,
},
}
const result = dataApiVersionResponseGuard(input)
expect(result).toEqual(input)
})

it('rejects empty object', () => {
expect(() => dataApiVersionResponseGuard({})).toThrowError()
expect(() =>
dataApiVersionResponseGuard({ data_api_version: {} })
).toThrowError()
})

it('rejects major missing', () => {
expect(() =>
dataApiVersionResponseGuard({
data_api_version: {
minor: 0,
},
})
).toThrowError()
})

it('rejects major is string not number', () => {
expect(() =>
dataApiVersionResponseGuard({
data_api_version: {
major: '4',
minor: 0,
},
})
).toThrowError()
})

it('rejects minor is string not number', () => {
expect(() =>
dataApiVersionResponseGuard({
data_api_version: {
major: 4,
minor: '0',
},
})
).toThrowError()
})

it('accepts minor missing', () => {
const expectedOutput = {
data_api_version: {
major: 4,
},
}
const result = dataApiVersionResponseGuard(expectedOutput)
expect(result).toEqual(expectedOutput)
})

it('removes extra keys in object', () => {
const expectedOutput = {
data_api_version: {
major: 4,
minor: 0,
},
}
const input = { ...expectedOutput, patch: 12 }
const result = dataApiVersionResponseGuard(input)
expect(result).not.toHaveProperty('patch')
expect(result).toEqual(expectedOutput)
})
})
})
9 changes: 9 additions & 0 deletions src/api/v4/apiv4decoders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,3 +70,12 @@ export const binnedQueryResponseGuard = guard(
counts: array(either3(integer, array(integer), array(array(integer)))),
})
)

export const dataApiVersionResponseGuard = guard(
object({
data_api_version: object({
major: integer,
minor: optional(integer),
}),
})
)

0 comments on commit 383442d

Please sign in to comment.