@thalorlabs/chucknorris 1.0.1 → 1.1.1

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/.prettierrc CHANGED
@@ -1,7 +1 @@
1
- {
2
- "semi": true,
3
- "singleQuote": true,
4
- "trailingComma": "es5",
5
- "printWidth": 80,
6
- "tabWidth": 2
7
- }
1
+ "@thalorlabs/eslint-plugin-dev-config/prettier"
@@ -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
- * Calls the external API and normalises the raw response to TLJoke.
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
- * Calls the external API and normalises the raw response to TLJoke.
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
- this.serviceName = 'chucknorris';
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 { data } = await this.axiosInstance.get('/jokes/random');
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, // Chuck Norris jokes can contain explicit content
34
+ safe: false,
41
35
  provider: this.serviceName,
42
36
  });
43
37
  }
package/eslint.config.mjs CHANGED
@@ -1,21 +1,3 @@
1
- import eslint from '@eslint/js';
2
- import tseslint from 'typescript-eslint';
3
- import prettier from 'eslint-config-prettier';
4
- import nodePlugin from 'eslint-plugin-n';
1
+ import { backend } from '@thalorlabs/eslint-plugin-dev-config';
5
2
 
6
- export default tseslint.config(
7
- eslint.configs.recommended,
8
- ...tseslint.configs.recommended,
9
- nodePlugin.configs['flat/recommended-module'],
10
- prettier,
11
- {
12
- rules: {
13
- '@typescript-eslint/no-explicit-any': 'error',
14
- 'n/no-missing-import': 'off',
15
- 'n/no-unpublished-import': 'off',
16
- },
17
- },
18
- {
19
- ignores: ['dist/', 'node_modules/'],
20
- }
21
- );
3
+ export default backend;
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@thalorlabs/chucknorris",
3
3
  "author": "ThalorLabs",
4
4
  "private": false,
5
- "version": "1.0.1",
5
+ "version": "1.1.1",
6
6
  "description": "Provider adapter for api.chucknorris.io — returns TLJoke",
7
7
  "homepage": "https://github.com/ThalorLabs/chucknorris#readme",
8
8
  "bugs": {
@@ -28,19 +28,15 @@
28
28
  "format:check": "prettier --check \"src/**/*.ts\""
29
29
  },
30
30
  "devDependencies": {
31
- "@eslint/js": "^10.0.1",
31
+ "@thalorlabs/eslint-plugin-dev-config": "^2.1.1",
32
32
  "@types/node": "^25.5.2",
33
- "eslint": "^10.1.0",
34
- "eslint-config-prettier": "^10.1.8",
35
- "eslint-plugin-n": "^17.24.0",
36
33
  "prettier": "^3.8.1",
37
34
  "rimraf": "^5.0.0",
38
35
  "typescript": "^5.0.0",
39
- "typescript-eslint": "^8.58.0",
40
36
  "vitest": "^3.0.0"
41
37
  },
42
38
  "dependencies": {
43
- "@thalorlabs/types": "^1.7.0",
44
- "axios": "^1.7.0"
39
+ "@thalorlabs/api": "^1.0.0",
40
+ "@thalorlabs/types": "^1.7.0"
45
41
  }
46
42
  }
@@ -1,5 +1,5 @@
1
- import axios, { AxiosInstance } from 'axios';
2
- import { createHash } from 'crypto';
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
- * Calls the external API and normalises the raw response to TLJoke.
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
- this.axiosInstance = axios.create({
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 { data } =
36
- await this.axiosInstance.get<ChuckNorrisRawResponse>('/jokes/random');
37
- return this.normalise(data);
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, // Chuck Norris jokes can contain explicit content
44
+ safe: false,
47
45
  provider: this.serviceName,
48
46
  });
49
47
  }
@@ -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 axios from 'axios';
4
+ import { BaseHttpClient } from '@thalorlabs/api';
5
5
 
6
- vi.mock('axios', () => {
7
- const mockAxiosInstance = {
8
- get: vi.fn(),
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
- const mockGet = getMockAxios();
61
- mockGet.mockResolvedValueOnce({
62
- data: {
63
- icon_url: 'https://api.chucknorris.io/img/avatar/chuck-norris.png',
64
- id: 'abc123',
65
- url: '',
66
- value: 'Chuck Norris can divide by zero.',
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
- mockGet.mockResolvedValue({
84
- data: {
85
- icon_url: 'https://example.com/icon.png',
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 on the API', async () => {
99
- const mockGet = getMockAxios();
100
- mockGet.mockResolvedValueOnce({
101
- data: {
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(mockGet).toHaveBeenCalledWith('/jokes/random');
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 API errors', async () => {
115
- const mockGet = getMockAxios();
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
  });
package/tsconfig.json CHANGED
@@ -1,13 +1,7 @@
1
1
  {
2
+ "extends": "@thalorlabs/eslint-plugin-dev-config/tsconfig/backend.json",
2
3
  "compilerOptions": {
3
- "target": "ES2020",
4
- "module": "CommonJS",
5
- "declaration": true,
6
- "outDir": "./dist",
7
- "strict": true,
8
- "esModuleInterop": true,
9
- "skipLibCheck": true,
10
- "moduleResolution": "node"
4
+ "outDir": "./dist"
11
5
  },
12
6
  "include": ["src"],
13
7
  "exclude": ["node_modules", "dist"]