Skip to content

Commit

Permalink
feat(http): Add getYaml and getYamlSafe methods
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Jan 11, 2025
1 parent 4466ccd commit fce95b9
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 4 deletions.
141 changes: 141 additions & 0 deletions lib/util/http/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -342,6 +342,147 @@ describe('util/http/index', () => {
memCache.reset();
});

describe('getPlain', () => {
it('gets plain text with correct headers', async () => {
httpMock.scope(baseUrl).get('/').reply(200, 'plain text response', {
'content-type': 'text/plain',
});

const res = await http.getPlain('http://renovate.com');
expect(res.body).toBe('plain text response');
expect(res.headers['content-type']).toBe('text/plain');
});

it('works with custom options', async () => {
httpMock
.scope(baseUrl)
.get('/')
.matchHeader('custom', 'header')
.reply(200, 'plain text response');

const res = await http.getPlain('http://renovate.com', {
headers: { custom: 'header' },
});
expect(res.body).toBe('plain text response');
});
});

describe('getYaml', () => {
it('parses yaml response without schema', async () => {
httpMock.scope(baseUrl).get('/').reply(200, 'x: 2\ny: 2');

const res = await http.getYaml('http://renovate.com');
expect(res.body).toEqual({ x: 2, y: 2 });
});

it('parses yaml with schema validation', async () => {
httpMock.scope(baseUrl).get('/').reply(200, 'x: 2\ny: 2');

const res = await http.getYaml('http://renovate.com', SomeSchema);
expect(res.body).toBe('2 + 2 = 4');
});

it('parses yaml with options and schema', async () => {
httpMock
.scope(baseUrl)
.get('/')
.matchHeader('custom', 'header')
.reply(200, 'x: 2\ny: 2');

const res = await http.getYaml(
'http://renovate.com',
{ headers: { custom: 'header' } },
SomeSchema,
);
expect(res.body).toBe('2 + 2 = 4');
});

it('throws on invalid yaml', async () => {
httpMock.scope(baseUrl).get('/').reply(200, '!@#$%^');

await expect(http.getYaml('http://renovate.com')).rejects.toThrow();
});

it('throws on schema validation failure', async () => {
httpMock.scope(baseUrl).get('/').reply(200, 'foo: bar');

await expect(
http.getYaml('http://renovate.com', SomeSchema),
).rejects.toThrow(z.ZodError);
});
});

describe('getYamlSafe', () => {
it('returns successful result with schema validation', async () => {
httpMock.scope('http://example.com').get('/').reply(200, 'x: 2\ny: 2');

const { val, err } = await http
.getYamlSafe('http://example.com', SomeSchema)
.unwrap();

expect(val).toBe('2 + 2 = 4');
expect(err).toBeUndefined();
});

it('returns schema error result', async () => {
httpMock
.scope('http://example.com')
.get('/')
.reply(200, 'x: "2"\ny: "2"');

const { val, err } = await http
.getYamlSafe('http://example.com', SomeSchema)
.unwrap();

expect(val).toBeUndefined();
expect(err).toBeInstanceOf(ZodError);
});

it('returns error result for invalid yaml', async () => {
httpMock.scope('http://example.com').get('/').reply(200, '!@#$%^');

const { val, err } = await http
.getYamlSafe('http://example.com', SomeSchema)
.unwrap();

expect(val).toBeUndefined();
expect(err).toBeDefined();
});

it('returns error result for network errors', async () => {
httpMock
.scope('http://example.com')
.get('/')
.replyWithError('network error');

const { val, err } = await http
.getYamlSafe('http://example.com', SomeSchema)
.unwrap();

expect(val).toBeUndefined();
expect(err).toBeInstanceOf(HttpError);
});

it('works with options and schema', async () => {
httpMock
.scope('http://example.com')
.get('/')
.matchHeader('custom', 'header')
.reply(200, 'x: 2\ny: 2');

const { val, err } = await http
.getYamlSafe(
'http://example.com',
{ headers: { custom: 'header' } },
SomeSchema,
)
.unwrap();

expect(val).toBe('2 + 2 = 4');
expect(err).toBeUndefined();
});
});

describe('getJson', () => {
it('uses schema for response body', async () => {
httpMock
Expand Down
84 changes: 80 additions & 4 deletions lib/util/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { hash } from '../hash';
import { type AsyncResult, Result } from '../result';
import { type HttpRequestStatsDataPoint, HttpStats } from '../stats';
import { resolveBaseUrl } from '../url';
import { parseSingleYaml } from '../yaml';
import { applyAuthorization, removeAuthorization } from './auth';
import { hooks } from './hooks';
import { applyHostRule, findMatchingRule } from './host-rules';
Expand All @@ -38,7 +39,7 @@ export { RequestError as HttpError };
export class EmptyResultError extends Error {}
export type SafeJsonError = RequestError | ZodError | EmptyResultError;

type JsonArgs<
type HttpFnArgs<
Opts extends HttpOptions,
ResT = unknown,
Schema extends ZodType<ResT> = ZodType<ResT>,
Expand Down Expand Up @@ -272,7 +273,7 @@ export class Http<Opts extends HttpOptions = HttpOptions> {

private async requestJson<ResT = unknown>(
method: InternalHttpOptions['method'],
{ url, httpOptions: requestOptions, schema }: JsonArgs<Opts, ResT>,
{ url, httpOptions: requestOptions, schema }: HttpFnArgs<Opts, ResT>,
): Promise<HttpResponse<ResT>> {
const { body, ...httpOptions } = { ...requestOptions };
const opts: InternalHttpOptions = {
Expand Down Expand Up @@ -302,8 +303,8 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
arg1: string,
arg2: Opts | ZodType<ResT> | undefined,
arg3: ZodType<ResT> | undefined,
): JsonArgs<Opts, ResT> {
const res: JsonArgs<Opts, ResT> = { url: arg1 };
): HttpFnArgs<Opts, ResT> {
const res: HttpFnArgs<Opts, ResT> = { url: arg1 };

if (arg2 instanceof ZodType) {
res.schema = arg2;
Expand All @@ -328,6 +329,81 @@ export class Http<Opts extends HttpOptions = HttpOptions> {
});
}

async getYaml<ResT>(url: string, options?: Opts): Promise<HttpResponse<ResT>>;
async getYaml<ResT, Schema extends ZodType<ResT> = ZodType<ResT>>(
url: string,
schema: Schema,
): Promise<HttpResponse<Infer<Schema>>>;
async getYaml<ResT, Schema extends ZodType<ResT> = ZodType<ResT>>(
url: string,
options: Opts,
schema: Schema,
): Promise<HttpResponse<Infer<Schema>>>;
async getYaml<ResT = unknown, Schema extends ZodType<ResT> = ZodType<ResT>>(
arg1: string,
arg2?: Opts | Schema,
arg3?: Schema,
): Promise<HttpResponse<ResT>> {
const { url, httpOptions, schema } = this.resolveArgs<ResT>(
arg1,
arg2,
arg3,
);
const opts: InternalHttpOptions = {
...httpOptions,
method: 'get',
};

const res = await this.get(url, opts);
if (!schema) {
const body = parseSingleYaml<ResT>(res.body);
return { ...res, body };
}

const body = await schema.parseAsync(parseSingleYaml(res.body));
return { ...res, body };
}

getYamlSafe<
ResT extends NonNullable<unknown>,
Schema extends ZodType<ResT> = ZodType<ResT>,
>(url: string, schema: Schema): AsyncResult<Infer<Schema>, SafeJsonError>;
getYamlSafe<
ResT extends NonNullable<unknown>,
Schema extends ZodType<ResT> = ZodType<ResT>,
>(
url: string,
options: Opts,
schema: Schema,
): AsyncResult<Infer<Schema>, SafeJsonError>;
getYamlSafe<
ResT extends NonNullable<unknown>,
Schema extends ZodType<ResT> = ZodType<ResT>,
>(
arg1: string,
arg2: Opts | Schema,
arg3?: Schema,
): AsyncResult<ResT, SafeJsonError> {
const url = arg1;
let schema: Schema;
let httpOptions: Opts | undefined;
if (arg3) {
schema = arg3;
httpOptions = arg2 as Opts;
} else {
schema = arg2 as Schema;
}

let res: AsyncResult<HttpResponse<ResT>, SafeJsonError>;
if (httpOptions) {
res = Result.wrap(this.getYaml<ResT>(url, httpOptions, schema));
} else {
res = Result.wrap(this.getYaml<ResT>(url, schema));
}

return res.transform((response) => Result.ok(response.body));
}

getJson<ResT>(url: string, options?: Opts): Promise<HttpResponse<ResT>>;
getJson<ResT, Schema extends ZodType<ResT> = ZodType<ResT>>(
url: string,
Expand Down

0 comments on commit fce95b9

Please sign in to comment.