Skip to content

Commit

Permalink
refactor(rubygems): Use Result class (#23522)
Browse files Browse the repository at this point in the history
  • Loading branch information
zharinov committed Jul 24, 2023
1 parent 3c999e7 commit 6451c3a
Show file tree
Hide file tree
Showing 5 changed files with 54 additions and 81 deletions.
18 changes: 3 additions & 15 deletions lib/modules/datasource/rubygems/index.spec.ts
Expand Up @@ -25,8 +25,6 @@ describe('modules/datasource/rubygems/index', () => {
it('returns null for missing pkg', async () => {
httpMock
.scope('https://example.com')
.get('/versions')
.reply(404)
.get('/api/v1/versions/foobar.json')
.reply(200, [])
.get('/api/v1/dependencies?gems=foobar')
Expand Down Expand Up @@ -117,17 +115,13 @@ describe('modules/datasource/rubygems/index', () => {
it('uses multiple source urls', async () => {
httpMock
.scope('https://registry-1.com/')
.get('/versions')
.reply(404)
.get('/api/v1/versions/foobar.json')
.reply(400)
.get('/api/v1/dependencies?gems=foobar')
.reply(404);

httpMock
.scope('https://registry-2.com/nested/path')
.get('/versions')
.reply(404)
.get('/api/v1/versions/foobar.json')
.reply(200, [
{ number: '1.0.0', created_at: '2021-01-01' },
Expand Down Expand Up @@ -160,8 +154,6 @@ describe('modules/datasource/rubygems/index', () => {
it('falls back to dependencies API', async () => {
httpMock
.scope('https://example.com/')
.get('/versions')
.reply(404)
.get('/api/v1/versions/foobar.json')
.reply(400, {})
.get('/api/v1/dependencies?gems=foobar')
Expand Down Expand Up @@ -194,10 +186,10 @@ describe('modules/datasource/rubygems/index', () => {
it('errors when version request fails with anything other than 400 or 404', async () => {
httpMock
.scope('https://example.com/')
.get('/versions')
.reply(404)
.get('/api/v1/versions/foobar.json')
.reply(500, {});
.reply(500, {})
.get('/api/v1/dependencies?gems=foobar')
.reply(500);

await expect(
getPkgReleases({
Expand All @@ -212,8 +204,6 @@ describe('modules/datasource/rubygems/index', () => {
it('returns null for GitHub Packages package miss', async () => {
httpMock
.scope('https://rubygems.pkg.github.com/example')
.get('/versions')
.reply(404)
.get('/api/v1/dependencies?gems=foobar')
.reply(200, rubyMarshal([]));

Expand All @@ -230,8 +220,6 @@ describe('modules/datasource/rubygems/index', () => {
it('returns a dep for GitHub Packages package hit', async () => {
httpMock
.scope('https://rubygems.pkg.github.com/example')
.get('/versions')
.reply(404)
.get('/api/v1/dependencies?gems=foobar')
.reply(
200,
Expand Down
88 changes: 39 additions & 49 deletions lib/modules/datasource/rubygems/index.ts
@@ -1,6 +1,7 @@
import { Marshal } from '@qnighy/marshal';
import { HttpError } from '../../../util/http';
import { Result } from '../../../util/result';
import type { ZodError } from 'zod';
import { logger } from '../../../logger';
import { AsyncResult, Result } from '../../../util/result';
import { getQueryString, joinUrlParts, parseUrl } from '../../../util/url';
import * as rubyVersioning from '../../versioning/ruby';
import { Datasource } from '../datasource';
Expand Down Expand Up @@ -40,64 +41,53 @@ export class RubyGemsDatasource extends Datasource {
return null;
}

const { val: rubygemsResult, err: rubygemsError } = await Result.wrap(
this.versionsEndpointCache.getVersions(registryUrl, packageName)
)
.transform((versions) =>
this.metadataCache.getRelease(registryUrl, packageName, versions)
)
.unwrap();
const registryHostname = parseUrl(registryUrl)?.hostname;

// istanbul ignore else: will be removed soon
if (rubygemsResult) {
return rubygemsResult;
} else if (rubygemsError instanceof Error) {
this.handleGenericErrors(rubygemsError);
let result: AsyncResult<ReleaseResult, Error | string>;
if (registryHostname === 'rubygems.org') {
result = Result.wrap(
this.versionsEndpointCache.getVersions(registryUrl, packageName)
).transform((versions) =>
this.metadataCache.getRelease(registryUrl, packageName, versions)
);
} else if (
registryHostname === 'rubygems.pkg.github.com' ||
registryHostname === 'gitlab.com'
) {
result = this.getReleasesViaDeprecatedAPI(registryUrl, packageName);
} else {
result = getV1Releases(this.http, registryUrl, packageName).catch(() =>
this.getReleasesViaDeprecatedAPI(registryUrl, packageName)
);
}

try {
const registryHostname = parseUrl(registryUrl)?.hostname;

if (
rubygemsError === 'unsupported-api' &&
registryHostname !== 'rubygems.org'
) {
if (
registryHostname === 'rubygems.pkg.github.com' ||
registryHostname === 'gitlab.com'
) {
return await this.getReleasesViaFallbackAPI(registryUrl, packageName);
}

const { val: apiV1Result, err: apiV1Error } = await getV1Releases(
this.http,
registryUrl,
packageName
).unwrap();
if (apiV1Result) {
return apiV1Result;
} else if (apiV1Error instanceof HttpError) {
throw apiV1Error;
}

return await this.getReleasesViaFallbackAPI(registryUrl, packageName);
}
const { val, err } = await result.unwrap();
if (val) {
return val;
}

return null;
} catch (error) {
this.handleGenericErrors(error);
if (err instanceof Error) {
this.handleGenericErrors(err);
}

logger.debug({ packageName, registryUrl }, `Rubygems fetch error: ${err}`);
return null;
}

async getReleasesViaFallbackAPI(
private getReleasesViaDeprecatedAPI(
registryUrl: string,
packageName: string
): Promise<ReleaseResult | null> {
): AsyncResult<ReleaseResult, Error | ZodError> {
const path = joinUrlParts(registryUrl, `/api/v1/dependencies`);
const query = getQueryString({ gems: packageName });
const url = `${path}?${query}`;
const { body: buffer } = await this.http.getBuffer(url);
const data = Marshal.parse(buffer);
return MarshalledVersionInfo.parse(data);
const bufPromise = this.http.getBuffer(url);
return Result.wrap(bufPromise).transform(({ body }) => {
const data = Marshal.parse(body);
const releases = MarshalledVersionInfo.safeParse(data);
return releases.success
? Result.ok({ releases: releases.data })
: Result.err(releases.error);
});
}
}
4 changes: 3 additions & 1 deletion lib/modules/datasource/rubygems/metadata-cache.ts
Expand Up @@ -55,7 +55,9 @@ export class MetadataCache {
getV1Releases(this.http, registryUrl, packageName).transform(saveCache)
)
.catch(() =>
Result.ok({ releases: versions.map((version) => ({ version })) })
Result.ok({
releases: versions.map((version) => ({ version })),
})
)
.unwrapOrThrow();
}
Expand Down
17 changes: 5 additions & 12 deletions lib/modules/datasource/rubygems/schema.spec.ts
Expand Up @@ -9,18 +9,11 @@ describe('modules/datasource/rubygems/schema', () => {
{ number: '3.0.0' },
];
const output = MarshalledVersionInfo.parse(input);
expect(output).toEqual({
releases: [
{ version: '1.0.0' },
{ version: '2.0.0' },
{ version: '3.0.0' },
],
});
});

it('parses empty input', () => {
const output = MarshalledVersionInfo.parse([]);
expect(output).toBeNull();
expect(output).toEqual([
{ version: '1.0.0' },
{ version: '2.0.0' },
{ version: '3.0.0' },
]);
});
});

Expand Down
8 changes: 4 additions & 4 deletions lib/modules/datasource/rubygems/schema.ts
Expand Up @@ -9,10 +9,10 @@ export const MarshalledVersionInfo = LooseArray(
number: z.string(),
})
.transform(({ number: version }) => ({ version }))
)
.transform((releases) => (releases.length === 0 ? null : { releases }))
.nullable()
.catch(null);
).refine(
(value) => !is.emptyArray(value),
'Empty response from `/v1/dependencies` endpoint'
);

export const GemMetadata = z
.object({
Expand Down

0 comments on commit 6451c3a

Please sign in to comment.