@thalorlabs/chucknorris 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/ChuckNorrisClient.d.ts +4 -6
- package/dist/ChuckNorrisClient.js +11 -17
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/package.json +3 -3
- package/src/ChuckNorrisClient.ts +14 -16
- package/src/index.ts +1 -1
- package/tests/ChuckNorrisClient.test.ts +29 -64
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
1
2
|
import { TLJoke } from '@thalorlabs/types';
|
|
2
3
|
export interface ChuckNorrisClientConfig {
|
|
3
4
|
baseURL?: string;
|
|
@@ -6,15 +7,12 @@ export interface ChuckNorrisClientConfig {
|
|
|
6
7
|
/**
|
|
7
8
|
* HTTP adapter for api.chucknorris.io.
|
|
8
9
|
*
|
|
9
|
-
*
|
|
10
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
11
|
+
* observability. Normalises the raw response to TLJoke.
|
|
10
12
|
* No DB, no cache, no logging — pure HTTP adapter.
|
|
11
|
-
*
|
|
12
|
-
* When @thalorlabs/api is available, this should extend BaseHttpClient
|
|
13
|
-
* instead of managing its own axios instance.
|
|
14
13
|
*/
|
|
15
|
-
export declare class ChuckNorrisClient {
|
|
14
|
+
export declare class ChuckNorrisClient extends BaseHttpClient {
|
|
16
15
|
readonly serviceName = "chucknorris";
|
|
17
|
-
private readonly axiosInstance;
|
|
18
16
|
constructor(config?: ChuckNorrisClientConfig);
|
|
19
17
|
getJoke(): Promise<TLJoke>;
|
|
20
18
|
private normalise;
|
|
@@ -1,35 +1,29 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.ChuckNorrisClient = void 0;
|
|
7
|
-
const axios_1 = __importDefault(require("axios"));
|
|
8
4
|
const crypto_1 = require("crypto");
|
|
5
|
+
const api_1 = require("@thalorlabs/api");
|
|
9
6
|
const types_1 = require("@thalorlabs/types");
|
|
10
7
|
/**
|
|
11
8
|
* HTTP adapter for api.chucknorris.io.
|
|
12
9
|
*
|
|
13
|
-
*
|
|
10
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
11
|
+
* observability. Normalises the raw response to TLJoke.
|
|
14
12
|
* No DB, no cache, no logging — pure HTTP adapter.
|
|
15
|
-
*
|
|
16
|
-
* When @thalorlabs/api is available, this should extend BaseHttpClient
|
|
17
|
-
* instead of managing its own axios instance.
|
|
18
13
|
*/
|
|
19
|
-
class ChuckNorrisClient {
|
|
14
|
+
class ChuckNorrisClient extends api_1.BaseHttpClient {
|
|
20
15
|
constructor(config = {}) {
|
|
21
|
-
|
|
22
|
-
this.axiosInstance = axios_1.default.create({
|
|
16
|
+
super({
|
|
23
17
|
baseURL: config.baseURL ?? 'https://api.chucknorris.io',
|
|
18
|
+
serviceName: 'chucknorris',
|
|
19
|
+
retries: 3,
|
|
24
20
|
timeout: config.timeout ?? 10000,
|
|
25
|
-
headers: {
|
|
26
|
-
Accept: 'application/json',
|
|
27
|
-
},
|
|
28
21
|
});
|
|
22
|
+
this.serviceName = 'chucknorris';
|
|
29
23
|
}
|
|
30
24
|
async getJoke() {
|
|
31
|
-
const {
|
|
32
|
-
return this.normalise(data);
|
|
25
|
+
const { response } = await this.handleRequest({ method: 'GET', url: '/jokes/random' }, { method: 'GET', url: '/jokes/random', requestId: (0, crypto_1.randomUUID)() });
|
|
26
|
+
return this.normalise(response.data);
|
|
33
27
|
}
|
|
34
28
|
normalise(raw) {
|
|
35
29
|
return types_1.TLJoke.parse({
|
|
@@ -37,7 +31,7 @@ class ChuckNorrisClient {
|
|
|
37
31
|
type: types_1.EJokeType.SINGLE,
|
|
38
32
|
joke: raw.value,
|
|
39
33
|
category: types_1.EJokeCategory.GENERAL,
|
|
40
|
-
safe: false,
|
|
34
|
+
safe: false,
|
|
41
35
|
provider: this.serviceName,
|
|
42
36
|
});
|
|
43
37
|
}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
|
@@ -4,6 +4,6 @@ exports.CHUCK_NORRIS_CONFIG = exports.ChuckNorrisClient = void 0;
|
|
|
4
4
|
var ChuckNorrisClient_1 = require("./ChuckNorrisClient");
|
|
5
5
|
Object.defineProperty(exports, "ChuckNorrisClient", { enumerable: true, get: function () { return ChuckNorrisClient_1.ChuckNorrisClient; } });
|
|
6
6
|
exports.CHUCK_NORRIS_CONFIG = {
|
|
7
|
-
cacheTtlMs:
|
|
7
|
+
cacheTtlMs: 0, // random endpoint — no caching
|
|
8
8
|
isBillable: false, // api.chucknorris.io is free
|
|
9
9
|
};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@thalorlabs/chucknorris",
|
|
3
3
|
"author": "ThalorLabs",
|
|
4
4
|
"private": false,
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.1.0",
|
|
6
6
|
"description": "Provider adapter for api.chucknorris.io — returns TLJoke",
|
|
7
7
|
"homepage": "https://github.com/ThalorLabs/chucknorris#readme",
|
|
8
8
|
"bugs": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"vitest": "^3.0.0"
|
|
41
41
|
},
|
|
42
42
|
"dependencies": {
|
|
43
|
-
"@thalorlabs/
|
|
44
|
-
"
|
|
43
|
+
"@thalorlabs/api": "^1.0.0",
|
|
44
|
+
"@thalorlabs/types": "^1.7.0"
|
|
45
45
|
}
|
|
46
46
|
}
|
package/src/ChuckNorrisClient.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import { createHash, randomUUID } from 'crypto';
|
|
2
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
3
3
|
import { TLJoke, EJokeType, EJokeCategory } from '@thalorlabs/types';
|
|
4
4
|
import { ChuckNorrisRawResponse } from './ChuckNorrisTypes';
|
|
5
5
|
|
|
@@ -11,30 +11,28 @@ export interface ChuckNorrisClientConfig {
|
|
|
11
11
|
/**
|
|
12
12
|
* HTTP adapter for api.chucknorris.io.
|
|
13
13
|
*
|
|
14
|
-
*
|
|
14
|
+
* Extends BaseHttpClient for retry logic, exponential backoff, and
|
|
15
|
+
* observability. Normalises the raw response to TLJoke.
|
|
15
16
|
* No DB, no cache, no logging — pure HTTP adapter.
|
|
16
|
-
*
|
|
17
|
-
* When @thalorlabs/api is available, this should extend BaseHttpClient
|
|
18
|
-
* instead of managing its own axios instance.
|
|
19
17
|
*/
|
|
20
|
-
export class ChuckNorrisClient {
|
|
18
|
+
export class ChuckNorrisClient extends BaseHttpClient {
|
|
21
19
|
public readonly serviceName = 'chucknorris';
|
|
22
|
-
private readonly axiosInstance: AxiosInstance;
|
|
23
20
|
|
|
24
21
|
constructor(config: ChuckNorrisClientConfig = {}) {
|
|
25
|
-
|
|
22
|
+
super({
|
|
26
23
|
baseURL: config.baseURL ?? 'https://api.chucknorris.io',
|
|
24
|
+
serviceName: 'chucknorris',
|
|
25
|
+
retries: 3,
|
|
27
26
|
timeout: config.timeout ?? 10000,
|
|
28
|
-
headers: {
|
|
29
|
-
Accept: 'application/json',
|
|
30
|
-
},
|
|
31
27
|
});
|
|
32
28
|
}
|
|
33
29
|
|
|
34
30
|
async getJoke(): Promise<TLJoke> {
|
|
35
|
-
const {
|
|
36
|
-
|
|
37
|
-
|
|
31
|
+
const { response } = await this.handleRequest<ChuckNorrisRawResponse>(
|
|
32
|
+
{ method: 'GET', url: '/jokes/random' },
|
|
33
|
+
{ method: 'GET', url: '/jokes/random', requestId: randomUUID() }
|
|
34
|
+
);
|
|
35
|
+
return this.normalise(response.data);
|
|
38
36
|
}
|
|
39
37
|
|
|
40
38
|
private normalise(raw: ChuckNorrisRawResponse): TLJoke {
|
|
@@ -43,7 +41,7 @@ export class ChuckNorrisClient {
|
|
|
43
41
|
type: EJokeType.SINGLE,
|
|
44
42
|
joke: raw.value,
|
|
45
43
|
category: EJokeCategory.GENERAL,
|
|
46
|
-
safe: false,
|
|
44
|
+
safe: false,
|
|
47
45
|
provider: this.serviceName,
|
|
48
46
|
});
|
|
49
47
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,24 +1,12 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
2
|
import { ChuckNorrisClient } from '../src/ChuckNorrisClient';
|
|
3
3
|
import { EJokeType, EJokeCategory } from '@thalorlabs/types';
|
|
4
|
-
import
|
|
4
|
+
import { BaseHttpClient } from '@thalorlabs/api';
|
|
5
5
|
|
|
6
|
-
vi.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
return {
|
|
11
|
-
default: {
|
|
12
|
-
create: vi.fn(() => mockAxiosInstance),
|
|
13
|
-
},
|
|
14
|
-
};
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
function getMockAxios() {
|
|
18
|
-
const instance = (axios.create as ReturnType<typeof vi.fn>).mock.results[0]
|
|
19
|
-
.value;
|
|
20
|
-
return instance.get as ReturnType<typeof vi.fn>;
|
|
21
|
-
}
|
|
6
|
+
const mockHandleRequest = vi.fn();
|
|
7
|
+
vi.spyOn(BaseHttpClient.prototype as any, 'handleRequest').mockImplementation(
|
|
8
|
+
mockHandleRequest
|
|
9
|
+
);
|
|
22
10
|
|
|
23
11
|
describe('ChuckNorrisClient', () => {
|
|
24
12
|
let client: ChuckNorrisClient;
|
|
@@ -32,39 +20,20 @@ describe('ChuckNorrisClient', () => {
|
|
|
32
20
|
it('sets serviceName to chucknorris', () => {
|
|
33
21
|
expect(client.serviceName).toBe('chucknorris');
|
|
34
22
|
});
|
|
35
|
-
|
|
36
|
-
it('creates axios instance with default config', () => {
|
|
37
|
-
expect(axios.create).toHaveBeenCalledWith({
|
|
38
|
-
baseURL: 'https://api.chucknorris.io',
|
|
39
|
-
timeout: 10000,
|
|
40
|
-
headers: { Accept: 'application/json' },
|
|
41
|
-
});
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('accepts custom baseURL and timeout', () => {
|
|
45
|
-
vi.clearAllMocks();
|
|
46
|
-
new ChuckNorrisClient({
|
|
47
|
-
baseURL: 'http://localhost:3000',
|
|
48
|
-
timeout: 5000,
|
|
49
|
-
});
|
|
50
|
-
expect(axios.create).toHaveBeenCalledWith({
|
|
51
|
-
baseURL: 'http://localhost:3000',
|
|
52
|
-
timeout: 5000,
|
|
53
|
-
headers: { Accept: 'application/json' },
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
23
|
});
|
|
57
24
|
|
|
58
25
|
describe('getJoke', () => {
|
|
59
26
|
it('returns a valid TLJoke with SINGLE type and GENERAL category', async () => {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
27
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
28
|
+
response: {
|
|
29
|
+
data: {
|
|
30
|
+
icon_url: 'https://api.chucknorris.io/img/avatar/chuck-norris.png',
|
|
31
|
+
id: 'abc123',
|
|
32
|
+
url: '',
|
|
33
|
+
value: 'Chuck Norris can divide by zero.',
|
|
34
|
+
},
|
|
67
35
|
},
|
|
36
|
+
durationMs: 50,
|
|
68
37
|
});
|
|
69
38
|
|
|
70
39
|
const joke = await client.getJoke();
|
|
@@ -78,15 +47,12 @@ describe('ChuckNorrisClient', () => {
|
|
|
78
47
|
});
|
|
79
48
|
|
|
80
49
|
it('generates a deterministic id from joke text', async () => {
|
|
81
|
-
const mockGet = getMockAxios();
|
|
82
50
|
const jokeText = 'Test joke';
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
icon_url: '
|
|
86
|
-
id: 'raw-id',
|
|
87
|
-
url: '',
|
|
88
|
-
value: jokeText,
|
|
51
|
+
mockHandleRequest.mockResolvedValue({
|
|
52
|
+
response: {
|
|
53
|
+
data: { icon_url: '', id: 'raw-id', url: '', value: jokeText },
|
|
89
54
|
},
|
|
55
|
+
durationMs: 10,
|
|
90
56
|
});
|
|
91
57
|
|
|
92
58
|
const joke1 = await client.getJoke();
|
|
@@ -95,25 +61,24 @@ describe('ChuckNorrisClient', () => {
|
|
|
95
61
|
expect(joke1.id).toBe(joke2.id);
|
|
96
62
|
});
|
|
97
63
|
|
|
98
|
-
it('calls GET /jokes/random
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
icon_url: '',
|
|
103
|
-
id: 'abc',
|
|
104
|
-
url: '',
|
|
105
|
-
value: 'A joke',
|
|
64
|
+
it('calls handleRequest with GET /jokes/random', async () => {
|
|
65
|
+
mockHandleRequest.mockResolvedValueOnce({
|
|
66
|
+
response: {
|
|
67
|
+
data: { icon_url: '', id: 'abc', url: '', value: 'A joke' },
|
|
106
68
|
},
|
|
69
|
+
durationMs: 10,
|
|
107
70
|
});
|
|
108
71
|
|
|
109
72
|
await client.getJoke();
|
|
110
73
|
|
|
111
|
-
expect(
|
|
74
|
+
expect(mockHandleRequest).toHaveBeenCalledWith(
|
|
75
|
+
expect.objectContaining({ method: 'GET', url: '/jokes/random' }),
|
|
76
|
+
expect.objectContaining({ method: 'GET', url: '/jokes/random' })
|
|
77
|
+
);
|
|
112
78
|
});
|
|
113
79
|
|
|
114
|
-
it('propagates
|
|
115
|
-
|
|
116
|
-
mockGet.mockRejectedValueOnce(new Error('Network error'));
|
|
80
|
+
it('propagates errors from handleRequest', async () => {
|
|
81
|
+
mockHandleRequest.mockRejectedValueOnce(new Error('Network error'));
|
|
117
82
|
|
|
118
83
|
await expect(client.getJoke()).rejects.toThrow('Network error');
|
|
119
84
|
});
|