Skip to content

Commit

Permalink
refactor(http): Add getYaml and getYamlSafe methods (#33578)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov authored Jan 13, 2025
1 parent c450f84 commit 76ff1df
Show file tree
Hide file tree
Showing 6 changed files with 257 additions and 45 deletions.
10 changes: 6 additions & 4 deletions lib/modules/datasource/conan/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,12 @@ export class ConanDatasource extends Datasource {
return null;
}
const url = `https://api.github.com/repos/conan-io/conan-center-index/contents/recipes/${conanName}/config.yml`;
const res = await this.githubHttp.get(url, {
headers: { accept: 'application/vnd.github.v3.raw' },
});
return ConanCenterReleases.parse(res.body);
const { body: result } = await this.githubHttp.getYaml(
url,
{ headers: { accept: 'application/vnd.github.v3.raw' } },
ConanCenterReleases,
);
return result;
}

@cache({
Expand Down
9 changes: 4 additions & 5 deletions lib/modules/datasource/conan/schema.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import { z } from 'zod';
import { LooseArray, Yaml } from '../../../util/schema-utils';
import { LooseArray } from '../../../util/schema-utils';
import type { ReleaseResult } from '../types';
import { conanDatasourceRegex } from './common';

export const ConanCenterReleases = Yaml.pipe(
z.object({
export const ConanCenterReleases = z
.object({
versions: z.record(z.string(), z.unknown()),
}),
)
})
.transform(
({ versions }): ReleaseResult => ({
releases: Object.keys(versions).map((version) => ({ version })),
Expand Down
48 changes: 24 additions & 24 deletions lib/modules/datasource/glasskube-packages/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,7 @@ import { joinUrlParts } from '../../../util/url';
import * as glasskubeVersioning from '../../versioning/glasskube';
import { Datasource } from '../datasource';
import type { GetReleasesConfig, ReleaseResult } from '../types';
import type { GlasskubePackageVersions } from './schema';
import {
GlasskubePackageManifestYaml,
GlasskubePackageVersionsYaml,
} from './schema';
import { GlasskubePackageManifest, GlasskubePackageVersions } from './schema';

export class GlasskubePackagesDatasource extends Datasource {
static readonly id = 'glasskube-packages';
Expand All @@ -33,42 +29,46 @@ export class GlasskubePackagesDatasource extends Datasource {
packageName,
registryUrl,
}: GetReleasesConfig): Promise<ReleaseResult | null> {
let versions: GlasskubePackageVersions;
const result: ReleaseResult = { releases: [] };

try {
const response = await this.http.get(
const { val: versions, err: versionsErr } = await this.http
.getYamlSafe(
joinUrlParts(registryUrl!, packageName, 'versions.yaml'),
);
versions = GlasskubePackageVersionsYaml.parse(response.body);
} catch (err) {
this.handleGenericErrors(err);
GlasskubePackageVersions,
)
.unwrap();

if (versionsErr) {
this.handleGenericErrors(versionsErr);
}

result.releases = versions.versions.map((it) => ({
version: it.version,
}));
result.tags = { latest: versions.latestVersion };

try {
const response = await this.http.get(
const { val: latestManifest, err: latestManifestErr } = await this.http
.getYamlSafe(
joinUrlParts(
registryUrl!,
packageName,
versions.latestVersion,
'package.yaml',
),
);
const latestManifest = GlasskubePackageManifestYaml.parse(response.body);
for (const ref of latestManifest?.references ?? []) {
if (ref.label.toLowerCase() === 'github') {
result.sourceUrl = ref.url;
} else if (ref.label.toLowerCase() === 'website') {
result.homepage = ref.url;
}
GlasskubePackageManifest,
)
.unwrap();

if (latestManifestErr) {
this.handleGenericErrors(latestManifestErr);
}

for (const ref of latestManifest?.references ?? []) {
if (ref.label.toLowerCase() === 'github') {
result.sourceUrl = ref.url;
} else if (ref.label.toLowerCase() === 'website') {
result.homepage = ref.url;
}
} catch (err) {
this.handleGenericErrors(err);
}

return result;
Expand Down
10 changes: 2 additions & 8 deletions lib/modules/datasource/glasskube-packages/schema.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { z } from 'zod';
import { Yaml } from '../../../util/schema-utils';

const GlasskubePackageVersions = z.object({
export const GlasskubePackageVersions = z.object({
latestVersion: z.string(),
versions: z.array(z.object({ version: z.string() })),
});

const GlasskubePackageManifest = z.object({
export const GlasskubePackageManifest = z.object({
references: z.optional(
z.array(
z.object({
Expand All @@ -16,8 +15,3 @@ const GlasskubePackageManifest = z.object({
),
),
});

export const GlasskubePackageVersionsYaml = Yaml.pipe(GlasskubePackageVersions);
export const GlasskubePackageManifestYaml = Yaml.pipe(GlasskubePackageManifest);

export type GlasskubePackageVersions = z.infer<typeof GlasskubePackageVersions>;
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
Loading

0 comments on commit 76ff1df

Please sign in to comment.