From 40f615e8ea80a007bc6471a839965dcc4af6adbe Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Thu, 10 Feb 2022 23:07:16 +0300
Subject: [PATCH] refactor(datasource/pod): Convert to class (#14133)

* refactor(datasource/pod): Convert to class

* Fix lint

* Fix
---
 lib/constants/platform.spec.ts   |   4 +-
 lib/datasource/api.ts            |   4 +-
 lib/datasource/pod/index.spec.ts |   6 +-
 lib/datasource/pod/index.ts      | 289 +++++++++++++++----------------
 lib/manager/cocoapods/extract.ts |   4 +-
 lib/manager/cocoapods/index.ts   |   4 +-
 6 files changed, 153 insertions(+), 158 deletions(-)

diff --git a/lib/constants/platform.spec.ts b/lib/constants/platform.spec.ts
index 30c8572b84..1e1f407f9c 100644
--- a/lib/constants/platform.spec.ts
+++ b/lib/constants/platform.spec.ts
@@ -4,7 +4,7 @@ import { id as GH_TAGS_DS } from '../datasource/github-tags';
 import { GitlabPackagesDatasource } from '../datasource/gitlab-packages';
 import { GitlabReleasesDatasource } from '../datasource/gitlab-releases';
 import { id as GL_TAGS_DS } from '../datasource/gitlab-tags';
-import { id as POD_DS } from '../datasource/pod';
+import { PodDatasource } from '../datasource/pod';
 import { id as GITHUB_CHANGELOG_ID } from '../workers/pr/changelog/github';
 import { id as GITLAB_CHANGELOG_ID } from '../workers/pr/changelog/gitlab';
 import {
@@ -36,7 +36,7 @@ describe('constants/platform', () => {
   it('should be part of the GITHUB_API_USING_HOST_TYPES ', () => {
     expect(GITHUB_API_USING_HOST_TYPES.includes(GH_TAGS_DS)).toBeTrue();
     expect(GITHUB_API_USING_HOST_TYPES.includes(GH_RELEASES_DS)).toBeTrue();
-    expect(GITHUB_API_USING_HOST_TYPES.includes(POD_DS)).toBeTrue();
+    expect(GITHUB_API_USING_HOST_TYPES.includes(PodDatasource.id)).toBeTrue();
     expect(
       GITHUB_API_USING_HOST_TYPES.includes(GITHUB_CHANGELOG_ID)
     ).toBeTrue();
diff --git a/lib/datasource/api.ts b/lib/datasource/api.ts
index 573a834f1d..43de52583e 100644
--- a/lib/datasource/api.ts
+++ b/lib/datasource/api.ts
@@ -28,7 +28,7 @@ import * as npm from './npm';
 import * as nuget from './nuget';
 import { OrbDatasource } from './orb';
 import * as packagist from './packagist';
-import * as pod from './pod';
+import { PodDatasource } from './pod';
 import { PypiDatasource } from './pypi';
 import * as repology from './repology';
 import { RubyVersionDatasource } from './ruby-version';
@@ -72,7 +72,7 @@ api.set('npm', npm);
 api.set('nuget', nuget);
 api.set('orb', new OrbDatasource());
 api.set('packagist', packagist);
-api.set('pod', pod);
+api.set(PodDatasource.id, new PodDatasource());
 api.set('pypi', new PypiDatasource());
 api.set('repology', repology);
 api.set('ruby-version', new RubyVersionDatasource());
diff --git a/lib/datasource/pod/index.spec.ts b/lib/datasource/pod/index.spec.ts
index e2aef58e02..f08703b49a 100644
--- a/lib/datasource/pod/index.spec.ts
+++ b/lib/datasource/pod/index.spec.ts
@@ -2,11 +2,11 @@ import { getPkgReleases } from '..';
 import * as httpMock from '../../../test/http-mock';
 import { EXTERNAL_HOST_ERROR } from '../../constants/error-messages';
 import * as rubyVersioning from '../../versioning/ruby';
-import * as pod from '.';
+import { PodDatasource } from '.';
 
 const config = {
   versioning: rubyVersioning.id,
-  datasource: pod.id,
+  datasource: PodDatasource.id,
   depName: 'foo',
   registryUrls: [],
 };
@@ -30,7 +30,7 @@ describe('datasource/pod/index', () => {
         .reply(404);
       expect(
         await getPkgReleases({
-          datasource: pod.id,
+          datasource: PodDatasource.id,
           depName: 'foobar',
           registryUrls: [],
         })
diff --git a/lib/datasource/pod/index.ts b/lib/datasource/pod/index.ts
index 61fcd4b5de..ee82add16b 100644
--- a/lib/datasource/pod/index.ts
+++ b/lib/datasource/pod/index.ts
@@ -2,26 +2,14 @@ import crypto from 'crypto';
 import { HOST_DISABLED } from '../../constants/error-messages';
 import { logger } from '../../logger';
 import { ExternalHostError } from '../../types/errors/external-host-error';
-import * as packageCache from '../../util/cache/package';
-import { Http } from '../../util/http';
+import { cache } from '../../util/cache/package/decorator';
 import { GithubHttp } from '../../util/http/github';
 import type { HttpError } from '../../util/http/types';
 import { newlineRegex, regEx } from '../../util/regex';
+import { Datasource } from '../datasource';
 import { massageGithubUrl } from '../metadata';
 import type { GetReleasesConfig, ReleaseResult } from '../types';
 
-export const id = 'pod';
-
-export const customRegistrySupport = true;
-export const defaultRegistryUrls = ['https://cdn.cocoapods.org'];
-export const registryStrategy = 'hunt';
-
-const cacheNamespace = `datasource-${id}`;
-const cacheMinutes = 30;
-
-const githubHttp = new GithubHttp(id);
-const http = new Http(id);
-
 // eslint-disable-next-line typescript-enum/no-enum, typescript-enum/no-const-enum
 const enum URLFormatOptions {
   WithShardWithSpec,
@@ -39,6 +27,10 @@ function shardParts(lookupName: string): string[] {
     .split('');
 }
 
+const githubRegex = regEx(
+  /(?<hostURL>(^https:\/\/[a-zA-z0-9-.]+))\/(?<account>[^/]+)\/(?<repo>[^/]+?)(\.git|\/.*)?$/
+);
+
 function releasesGithubUrl(
   lookupName: string,
   opts: {
@@ -85,159 +77,162 @@ function handleError(lookupName: string, err: HttpError): void {
   }
 }
 
-async function requestCDN(
-  url: string,
-  lookupName: string
-): Promise<string | null> {
-  try {
-    const resp = await http.get(url);
-    if (resp?.body) {
-      return resp.body;
-    }
-  } catch (err) {
-    handleError(lookupName, err);
+function isDefaultRepo(url: string): boolean {
+  const match = githubRegex.exec(url);
+  if (match) {
+    const { account, repo } = match.groups || {};
+    return (
+      account.toLowerCase() === 'cocoapods' && repo.toLowerCase() === 'specs'
+    ); // https://github.com/CocoaPods/Specs.git
   }
+  return false;
+}
 
-  return null;
+function releasesCDNUrl(lookupName: string, registryUrl: string): string {
+  const shard = shardParts(lookupName).join('_');
+  return `${registryUrl}/all_pods_versions_${shard}.txt`;
 }
 
-async function requestGithub<T = unknown>(
-  url: string,
-  lookupName: string
-): Promise<T | null> {
-  try {
-    const resp = await githubHttp.getJson<T>(url);
-    if (resp?.body) {
-      return resp.body;
-    }
-  } catch (err) {
-    handleError(lookupName, err);
-  }
+export class PodDatasource extends Datasource {
+  static readonly id = 'pod';
 
-  return null;
-}
+  override readonly defaultRegistryUrls = ['https://cdn.cocoapods.org'];
 
-const githubRegex = regEx(
-  /(?<hostURL>(^https:\/\/[a-zA-z0-9-.]+))\/(?<account>[^/]+)\/(?<repo>[^/]+?)(\.git|\/.*)?$/
-);
+  override readonly registryStrategy = 'hunt';
 
-async function getReleasesFromGithub(
-  lookupName: string,
-  opts: { hostURL: string; account: string; repo: string },
-  useShard = true,
-  useSpecs = true,
-  urlFormatOptions = URLFormatOptions.WithShardWithSpec
-): Promise<ReleaseResult | null> {
-  const url = releasesGithubUrl(lookupName, { ...opts, useShard, useSpecs });
-  const resp = await requestGithub<{ name: string }[]>(url, lookupName);
-  if (resp) {
-    const releases = resp.map(({ name }) => ({ version: name }));
-    return { releases };
-  }
+  githubHttp: GithubHttp;
 
-  // iterating through enum to support different url formats
-  switch (urlFormatOptions) {
-    case URLFormatOptions.WithShardWithSpec:
-      return getReleasesFromGithub(
-        lookupName,
-        opts,
-        true,
-        false,
-        URLFormatOptions.WithShardWithoutSpec
-      );
-    case URLFormatOptions.WithShardWithoutSpec:
-      return getReleasesFromGithub(
-        lookupName,
-        opts,
-        false,
-        true,
-        URLFormatOptions.WithSpecsWithoutShard
-      );
-    case URLFormatOptions.WithSpecsWithoutShard:
-      return getReleasesFromGithub(
-        lookupName,
-        opts,
-        false,
-        false,
-        URLFormatOptions.WithoutSpecsWithoutShard
-      );
-    case URLFormatOptions.WithoutSpecsWithoutShard:
-    default:
-      return null;
+  constructor() {
+    super(PodDatasource.id);
+    this.githubHttp = new GithubHttp(PodDatasource.id);
   }
-}
 
-function releasesCDNUrl(lookupName: string, registryUrl: string): string {
-  const shard = shardParts(lookupName).join('_');
-  return `${registryUrl}/all_pods_versions_${shard}.txt`;
-}
-
-async function getReleasesFromCDN(
-  lookupName: string,
-  registryUrl: string
-): Promise<ReleaseResult | null> {
-  const url = releasesCDNUrl(lookupName, registryUrl);
-  const resp = await requestCDN(url, lookupName);
-  if (resp) {
-    const lines = resp.split(newlineRegex);
-    for (let idx = 0; idx < lines.length; idx += 1) {
-      const line = lines[idx];
-      const [name, ...versions] = line.split('/');
-      if (name === lookupName.replace(regEx(/\/.*$/), '')) {
-        const releases = versions.map((version) => ({ version }));
-        return { releases };
+  private async requestCDN(
+    url: string,
+    lookupName: string
+  ): Promise<string | null> {
+    try {
+      const resp = await this.http.get(url);
+      if (resp?.body) {
+        return resp.body;
       }
+    } catch (err) {
+      handleError(lookupName, err);
     }
-  }
-  return null;
-}
 
-function isDefaultRepo(url: string): boolean {
-  const match = githubRegex.exec(url);
-  if (match) {
-    const { account, repo } = match.groups || {};
-    return (
-      account.toLowerCase() === 'cocoapods' && repo.toLowerCase() === 'specs'
-    ); // https://github.com/CocoaPods/Specs.git
+    return null;
   }
-  return false;
-}
 
-export async function getReleases({
-  lookupName,
-  registryUrl,
-}: GetReleasesConfig): Promise<ReleaseResult | null> {
-  const podName = lookupName.replace(regEx(/\/.*$/), '');
-
-  const cachedResult = await packageCache.get<ReleaseResult>(
-    cacheNamespace,
-    registryUrl + podName
-  );
-
-  // istanbul ignore if
-  if (cachedResult !== undefined) {
-    logger.trace(`CocoaPods: Return cached result for ${podName}`);
-    return cachedResult;
+  private async requestGithub<T = unknown>(
+    url: string,
+    lookupName: string
+  ): Promise<T | null> {
+    try {
+      const resp = await this.githubHttp.getJson<T>(url);
+      if (resp?.body) {
+        return resp.body;
+      }
+    } catch (err) {
+      handleError(lookupName, err);
+    }
+
+    return null;
   }
 
-  let baseUrl = registryUrl.replace(regEx(/\/+$/), '');
-  baseUrl = massageGithubUrl(baseUrl);
-  // In order to not abuse github API limits, query CDN instead
-  if (isDefaultRepo(baseUrl)) {
-    [baseUrl] = defaultRegistryUrls;
+  private async getReleasesFromGithub(
+    lookupName: string,
+    opts: { hostURL: string; account: string; repo: string },
+    useShard = true,
+    useSpecs = true,
+    urlFormatOptions = URLFormatOptions.WithShardWithSpec
+  ): Promise<ReleaseResult | null> {
+    const url = releasesGithubUrl(lookupName, { ...opts, useShard, useSpecs });
+    const resp = await this.requestGithub<{ name: string }[]>(url, lookupName);
+    if (resp) {
+      const releases = resp.map(({ name }) => ({ version: name }));
+      return { releases };
+    }
+
+    // iterating through enum to support different url formats
+    switch (urlFormatOptions) {
+      case URLFormatOptions.WithShardWithSpec:
+        return this.getReleasesFromGithub(
+          lookupName,
+          opts,
+          true,
+          false,
+          URLFormatOptions.WithShardWithoutSpec
+        );
+      case URLFormatOptions.WithShardWithoutSpec:
+        return this.getReleasesFromGithub(
+          lookupName,
+          opts,
+          false,
+          true,
+          URLFormatOptions.WithSpecsWithoutShard
+        );
+      case URLFormatOptions.WithSpecsWithoutShard:
+        return this.getReleasesFromGithub(
+          lookupName,
+          opts,
+          false,
+          false,
+          URLFormatOptions.WithoutSpecsWithoutShard
+        );
+      case URLFormatOptions.WithoutSpecsWithoutShard:
+      default:
+        return null;
+    }
   }
 
-  let result: ReleaseResult | null = null;
-  const match = githubRegex.exec(baseUrl);
-  if (match) {
-    const { hostURL, account, repo } = match?.groups || {};
-    const opts = { hostURL, account, repo };
-    result = await getReleasesFromGithub(podName, opts);
-  } else {
-    result = await getReleasesFromCDN(podName, baseUrl);
+  private async getReleasesFromCDN(
+    lookupName: string,
+    registryUrl: string
+  ): Promise<ReleaseResult | null> {
+    const url = releasesCDNUrl(lookupName, registryUrl);
+    const resp = await this.requestCDN(url, lookupName);
+    if (resp) {
+      const lines = resp.split(newlineRegex);
+      for (let idx = 0; idx < lines.length; idx += 1) {
+        const line = lines[idx];
+        const [name, ...versions] = line.split('/');
+        if (name === lookupName.replace(regEx(/\/.*$/), '')) {
+          const releases = versions.map((version) => ({ version }));
+          return { releases };
+        }
+      }
+    }
+    return null;
   }
 
-  await packageCache.set(cacheNamespace, podName, result, cacheMinutes);
+  @cache({
+    ttlMinutes: 30,
+    namespace: `datasource-${PodDatasource.id}`,
+    key: ({ lookupName, registryUrl }: GetReleasesConfig) =>
+      `${registryUrl}:${lookupName}`,
+  })
+  async getReleases({
+    lookupName,
+    registryUrl,
+  }: GetReleasesConfig): Promise<ReleaseResult | null> {
+    const podName = lookupName.replace(regEx(/\/.*$/), '');
+    let baseUrl = registryUrl.replace(regEx(/\/+$/), '');
+    baseUrl = massageGithubUrl(baseUrl);
+    // In order to not abuse github API limits, query CDN instead
+    if (isDefaultRepo(baseUrl)) {
+      [baseUrl] = this.defaultRegistryUrls;
+    }
 
-  return result;
+    let result: ReleaseResult | null = null;
+    const match = githubRegex.exec(baseUrl);
+    if (match) {
+      const { hostURL, account, repo } = match?.groups || {};
+      const opts = { hostURL, account, repo };
+      result = await this.getReleasesFromGithub(podName, opts);
+    } else {
+      result = await this.getReleasesFromCDN(podName, baseUrl);
+    }
+
+    return result;
+  }
 }
diff --git a/lib/manager/cocoapods/extract.ts b/lib/manager/cocoapods/extract.ts
index 5c4ad4d668..f979086478 100644
--- a/lib/manager/cocoapods/extract.ts
+++ b/lib/manager/cocoapods/extract.ts
@@ -1,7 +1,7 @@
 import { GitTagsDatasource } from '../../datasource/git-tags';
 import * as datasourceGithubTags from '../../datasource/github-tags';
 import * as datasourceGitlabTags from '../../datasource/gitlab-tags';
-import * as datasourcePod from '../../datasource/pod';
+import { PodDatasource } from '../../datasource/pod';
 import { logger } from '../../logger';
 import { getSiblingFileName, localPathExists } from '../../util/fs';
 import { newlineRegex, regEx } from '../../util/regex';
@@ -119,7 +119,7 @@ export async function extractPackageFile(
         dep = {
           depName,
           groupName,
-          datasource: datasourcePod.id,
+          datasource: PodDatasource.id,
           currentValue,
           managerData,
           registryUrls,
diff --git a/lib/manager/cocoapods/index.ts b/lib/manager/cocoapods/index.ts
index 48284b915f..13c6be03d9 100644
--- a/lib/manager/cocoapods/index.ts
+++ b/lib/manager/cocoapods/index.ts
@@ -1,7 +1,7 @@
 import { GitTagsDatasource } from '../../datasource/git-tags';
 import * as datasourceGithubTags from '../../datasource/github-tags';
 import * as datasourceGitlabTags from '../../datasource/gitlab-tags';
-import * as datasourcePod from '../../datasource/pod';
+import { PodDatasource } from '../../datasource/pod';
 import * as rubyVersioning from '../../versioning/ruby';
 
 export { extractPackageFile } from './extract';
@@ -16,5 +16,5 @@ export const supportedDatasources = [
   GitTagsDatasource.id,
   datasourceGithubTags.id,
   datasourceGitlabTags.id,
-  datasourcePod.id,
+  PodDatasource.id,
 ];
-- 
GitLab