Skip to content

Commit

Permalink
feat: Introduce GaxiosOptionsPrepared for improved type guarantees
Browse files Browse the repository at this point in the history
  • Loading branch information
d-goog committed May 16, 2024
1 parent 9164b87 commit 84faea8
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 43 deletions.
14 changes: 10 additions & 4 deletions src/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ export interface Headers {
export type GaxiosPromise<T = any> = Promise<GaxiosResponse<T>>;

Check warning on line 128 in src/common.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type

export interface GaxiosResponse<T = any> extends Response {

Check warning on line 130 in src/common.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
config: GaxiosOptions;
config: GaxiosOptionsPrepared;
data: T;
}

Expand All @@ -148,8 +148,8 @@ export interface GaxiosOptions extends Omit<RequestInit, 'headers'> {
* @deprecated Use {@link GaxiosOptions.fetchImplementation} instead.
*/
adapter?: <T = any>(

Check warning on line 150 in src/common.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected any. Specify a different type
options: GaxiosOptions,
defaultAdapter: (options: GaxiosOptions) => GaxiosPromise<T>
options: GaxiosOptionsPrepared,
defaultAdapter: (options: GaxiosOptionsPrepared) => GaxiosPromise<T>
) => GaxiosPromise<T>;
url?: string | URL;
/**
Expand Down Expand Up @@ -330,13 +330,19 @@ export interface GaxiosOptions extends Omit<RequestInit, 'headers'> {
*/
errorRedactor?: typeof defaultErrorRedactor | false;
}

export interface GaxiosOptionsPrepared extends GaxiosOptions {
headers: globalThis.Headers;
url: NonNullable<GaxiosOptions['url']>;
}

/**
* A partial object of `GaxiosOptions` with only redactable keys
*
* @experimental
*/
export type RedactableGaxiosOptions = Pick<
GaxiosOptions,
GaxiosOptions | GaxiosOptionsPrepared,
'body' | 'data' | 'headers' | 'url'
>;
/**
Expand Down
34 changes: 14 additions & 20 deletions src/gaxios.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
GaxiosMultipartOptions,
GaxiosError,
GaxiosOptions,
GaxiosOptionsPrepared,
GaxiosPromise,
GaxiosResponse,
Headers,
Expand Down Expand Up @@ -50,7 +51,7 @@ export class Gaxios {
* Interceptors
*/
interceptors: {
request: GaxiosInterceptorManager<GaxiosOptions>;
request: GaxiosInterceptorManager<GaxiosOptionsPrepared>;
response: GaxiosInterceptorManager<GaxiosResponse>;
};

Expand All @@ -77,7 +78,7 @@ export class Gaxios {
}

private async _defaultAdapter<T>(
config: GaxiosOptions
config: GaxiosOptionsPrepared
): Promise<GaxiosResponse<T>> {
const fetchImpl =
config.fetchImplementation ||
Expand All @@ -89,7 +90,7 @@ export class Gaxios {
const preparedOpts = {...config};
delete preparedOpts.data;

const res = (await fetchImpl(config.url!, preparedOpts as {})) as Response;
const res = (await fetchImpl(config.url, preparedOpts as {})) as Response;
let data = await this.getResponseData(config, res);

// `node-fetch`'s data isn't writable. Native `fetch`'s is.
Expand Down Expand Up @@ -123,7 +124,7 @@ export class Gaxios {
* @param opts Set of HTTP options that will be used for this HTTP request.
*/
protected async _request<T = any>(
opts: GaxiosOptions = {}
opts: GaxiosOptionsPrepared
): GaxiosPromise<T> {
try {
let translatedResponse: GaxiosResponse<T>;
Expand Down Expand Up @@ -175,7 +176,7 @@ export class Gaxios {
}

private async getResponseData(
opts: GaxiosOptions,
opts: GaxiosOptionsPrepared,
res: Response
): Promise<any> {
if (
Expand Down Expand Up @@ -262,16 +263,16 @@ export class Gaxios {
* @returns {Promise<GaxiosOptions>} Promise that resolves to the set of options or response after interceptors are applied.
*/
async #applyRequestInterceptors(
options: GaxiosOptions
): Promise<GaxiosOptions> {
options: GaxiosOptionsPrepared
): Promise<GaxiosOptionsPrepared> {
let promiseChain = Promise.resolve(options);

for (const interceptor of this.interceptors.request.values()) {
if (interceptor) {
promiseChain = promiseChain.then(
interceptor.resolved,
interceptor.rejected
) as Promise<GaxiosOptions>;
) as Promise<GaxiosOptionsPrepared>;
}
}

Expand Down Expand Up @@ -309,7 +310,9 @@ export class Gaxios {
* @param options The original options passed from the client.
* @returns Prepared options, ready to make a request
*/
async #prepareRequest(options: GaxiosOptions): Promise<GaxiosOptions> {
async #prepareRequest(
options: GaxiosOptions
): Promise<GaxiosOptionsPrepared> {
const opts: GaxiosOptions = extend(true, {}, this.defaults, options);
if (!opts.url) {
throw new Error('URL is required.');
Expand Down Expand Up @@ -476,18 +479,9 @@ export class Gaxios {
(opts as {duplex: string}).duplex = 'half';
}

// preserve the original type for auditing later
if (opts.headers instanceof Headers) {
opts.headers = preparedHeaders;
} else {
const headers: Headers = {};
preparedHeaders.forEach((value, key) => {
headers[key] = value;
});
opts.headers = headers;
}
opts.headers = preparedHeaders;

return opts;
return opts as GaxiosOptionsPrepared;
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export {
GaxiosError,
GaxiosPromise,
GaxiosResponse,
GaxiosOptionsPrepared as PreparedGaxiosOptions,
Headers,
RetryConfig,
} from './common';
Expand Down
8 changes: 5 additions & 3 deletions src/interceptor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
// See the License for the specific language governing permissions and
// limitations under the License.

import {GaxiosError, GaxiosOptions, GaxiosResponse} from './common';
import {GaxiosError, GaxiosOptionsPrepared, GaxiosResponse} from './common';

/**
* Interceptors that can be run for requests or responses. These interceptors run asynchronously.
*/
export interface GaxiosInterceptor<T extends GaxiosOptions | GaxiosResponse> {
export interface GaxiosInterceptor<
T extends GaxiosOptionsPrepared | GaxiosResponse,
> {
/**
* Function to be run when applying an interceptor.
*
Expand All @@ -37,5 +39,5 @@ export interface GaxiosInterceptor<T extends GaxiosOptions | GaxiosResponse> {
* Class to manage collections of GaxiosInterceptors for both requests and responses.
*/
export class GaxiosInterceptorManager<
T extends GaxiosOptions | GaxiosResponse,
T extends GaxiosOptionsPrepared | GaxiosResponse,
> extends Set<GaxiosInterceptor<T> | null> {}
36 changes: 20 additions & 16 deletions test/test.getch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,11 @@ import {
GaxiosResponse,
GaxiosPromise,
} from '../src';
import {GAXIOS_ERROR_SYMBOL, Headers} from '../src/common';
import {
GAXIOS_ERROR_SYMBOL,
GaxiosOptionsPrepared,
Headers,
} from '../src/common';
import {pkg} from '../src/util';
import fs from 'fs';

Expand Down Expand Up @@ -155,8 +159,8 @@ describe('🥁 configuration options', () => {
const inst = new Gaxios({headers: {apple: 'juice'}});
const res = await inst.request({url, headers: {figgy: 'pudding'}});
scope.done();
assert.strictEqual(res.config.headers!.apple, 'juice');
assert.strictEqual(res.config.headers!.figgy, 'pudding');
assert.strictEqual(res.config.headers.get('apple'), 'juice');
assert.strictEqual(res.config.headers.get('figgy'), 'pudding');
});

it('should allow setting a base url in the options', async () => {
Expand Down Expand Up @@ -1125,7 +1129,7 @@ describe('🍂 defaults & instances', () => {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected async _request<T = any>(
opts: GaxiosOptions = {}
opts: GaxiosOptionsPrepared
): GaxiosPromise<T> {
assert(opts.agent);
return super._request(opts);
Expand All @@ -1141,8 +1145,8 @@ describe('🍂 defaults & instances', () => {
});
const res = await inst.request({url, headers: {figgy: 'pudding'}});
scope.done();
assert.strictEqual(res.config.headers!.apple, 'juice');
assert.strictEqual(res.config.headers!.figgy, 'pudding');
assert.strictEqual(res.config.headers.get('apple'), 'juice');
assert.strictEqual(res.config.headers.get('figgy'), 'pudding');
const agentCache = inst.getAgentCache();
assert(agentCache.get(key));
});
Expand Down Expand Up @@ -1173,7 +1177,7 @@ describe('interceptors', () => {
const instance = new Gaxios();
instance.interceptors.request.add({
resolved: config => {
config.headers = {hello: 'world'};
config.headers.set('hello', 'world');
return Promise.resolve(config);
},
});
Expand All @@ -1190,7 +1194,7 @@ describe('interceptors', () => {
validateStatus: () => {
return true;
},
}) as unknown as Promise<GaxiosOptions>
}) as unknown as Promise<GaxiosOptionsPrepared>
);
const instance = new Gaxios();
const interceptor = {resolved: spyFunc};
Expand All @@ -1212,22 +1216,22 @@ describe('interceptors', () => {
const instance = new Gaxios();
instance.interceptors.request.add({
resolved: config => {
config.headers!['foo'] = 'bar';
config.headers.set('foo', 'bar');
return Promise.resolve(config);
},
});
instance.interceptors.request.add({
resolved: config => {
assert.strictEqual(config.headers!['foo'], 'bar');
config.headers!['bar'] = 'baz';
assert.strictEqual(config.headers.get('foo'), 'bar');
config.headers.set('bar', 'baz');
return Promise.resolve(config);
},
});
instance.interceptors.request.add({
resolved: config => {
assert.strictEqual(config.headers!['foo'], 'bar');
assert.strictEqual(config.headers!['bar'], 'baz');
config.headers!['baz'] = 'buzz';
assert.strictEqual(config.headers.get('foo'), 'bar');
assert.strictEqual(config.headers.get('bar'), 'baz');
config.headers.set('baz', 'buzz');
return Promise.resolve(config);
},
});
Expand All @@ -1244,7 +1248,7 @@ describe('interceptors', () => {
validateStatus: () => {
return true;
},
}) as unknown as Promise<GaxiosOptions>
}) as unknown as Promise<GaxiosOptionsPrepared>
);
const instance = new Gaxios();
instance.interceptors.request.add({
Expand Down Expand Up @@ -1272,7 +1276,7 @@ describe('interceptors', () => {
});
instance.interceptors.request.add({
resolved: config => {
config.headers = {hello: 'world'};
config.headers.set('hello', 'world');
return Promise.resolve(config);
},
rejected: err => {
Expand Down

0 comments on commit 84faea8

Please sign in to comment.