From edef60045c48030a611fbdfbf7d970a86f4126c2 Mon Sep 17 00:00:00 2001
From: Sergei Zharinov <zharinov@users.noreply.github.com>
Date: Wed, 22 Feb 2023 17:45:26 +0300
Subject: [PATCH] feat(schema): Add `looseValue` and `looseObject` helpers
 (#20576)

---
 lib/modules/datasource/packagist/schema.ts | 29 +++++---------
 lib/util/schema.spec.ts                    | 46 ++++++++++++++++++++++
 lib/util/schema.ts                         | 29 ++++++++++++++
 3 files changed, 84 insertions(+), 20 deletions(-)

diff --git a/lib/modules/datasource/packagist/schema.ts b/lib/modules/datasource/packagist/schema.ts
index 9e15d5bf15..166a668d5d 100644
--- a/lib/modules/datasource/packagist/schema.ts
+++ b/lib/modules/datasource/packagist/schema.ts
@@ -1,6 +1,7 @@
 import is from '@sindresorhus/is';
 import { z } from 'zod';
 import { logger } from '../../../logger';
+import { looseObject, looseValue } from '../../../util/schema';
 import type { Release, ReleaseResult } from '../types';
 
 export const MinifiedArray = z.array(z.record(z.unknown())).transform((xs) => {
@@ -44,32 +45,20 @@ export const ComposerRelease = z
     version: z.string(),
   })
   .merge(
-    z
-      .object({
-        homepage: z.string().nullable().catch(null),
-        source: z
-          .object({
-            url: z.string(),
-          })
-          .nullable()
-          .catch(null),
-        time: z.string().nullable().catch(null),
-        require: z
-          .object({
-            php: z.string(),
-          })
-          .nullable()
-          .catch(null),
-      })
-      .partial()
+    looseObject({
+      homepage: z.string(),
+      source: z.object({ url: z.string() }),
+      time: z.string(),
+      require: z.object({ php: z.string() }),
+    })
   );
 export type ComposerRelease = z.infer<typeof ComposerRelease>;
 
 export const ComposerReleases = z
   .union([
-    z.array(ComposerRelease.nullable().catch(null)),
+    z.array(looseValue(ComposerRelease)),
     z
-      .record(ComposerRelease.nullable().catch(null))
+      .record(looseValue(ComposerRelease))
       .transform((map) => Object.values(map)),
   ])
   .catch([])
diff --git a/lib/util/schema.spec.ts b/lib/util/schema.spec.ts
index a8c4221144..39096a1168 100644
--- a/lib/util/schema.spec.ts
+++ b/lib/util/schema.spec.ts
@@ -151,4 +151,50 @@ describe('util/schema', () => {
       expect(called).toBeTrue();
     });
   });
+
+  describe('looseValue', () => {
+    it('parses value', () => {
+      const s = schema.looseValue(z.string());
+      expect(s.parse('foobar')).toBe('foobar');
+    });
+
+    it('falls back to null wrong value', () => {
+      const s = schema.looseValue(z.string());
+      expect(s.parse(123)).toBeNull();
+    });
+
+    it('runs callback for wrong elements', () => {
+      let called = false;
+      const s = schema.looseValue(z.string(), () => {
+        called = true;
+      });
+      expect(s.parse(123)).toBeNull();
+      expect(called).toBeTrue();
+    });
+  });
+
+  describe('looseObject', () => {
+    it('parses object', () => {
+      const s = schema.looseObject({
+        foo: z.string(),
+        bar: z.number(),
+      });
+      expect(s.parse({ foo: 'foo', bar: 123 })).toEqual({
+        foo: 'foo',
+        bar: 123,
+      });
+    });
+
+    it('drops wrong items', () => {
+      const s = schema.looseObject({
+        foo: z.string(),
+        bar: z.number(),
+        baz: z.string(),
+      });
+      expect(s.parse({ foo: 'foo', bar: 'bar' })).toEqual({
+        foo: 'foo',
+        bar: null,
+      });
+    });
+  });
 });
diff --git a/lib/util/schema.ts b/lib/util/schema.ts
index 7b0756def4..8d369a33b2 100644
--- a/lib/util/schema.ts
+++ b/lib/util/schema.ts
@@ -143,3 +143,32 @@ export function looseRecord<T extends z.ZodTypeAny>(
 
   return filteredRecord;
 }
+
+export function looseValue<T, U extends z.ZodTypeDef, V>(
+  schema: z.ZodType<T, U, V>,
+  catchCallback?: () => void
+): z.ZodCatch<z.ZodNullable<z.ZodType<T, U, V>>> {
+  const nullableSchema = schema.nullable();
+  const schemaWithFallback = catchCallback
+    ? nullableSchema.catch(() => {
+        catchCallback();
+        return null;
+      })
+    : nullableSchema.catch(null);
+  return schemaWithFallback;
+}
+
+export function looseObject<T extends z.ZodRawShape>(
+  shape: T
+): z.ZodObject<{
+  [k in keyof T]: z.ZodOptional<z.ZodCatch<z.ZodNullable<T[k]>>>;
+}> {
+  const newShape: Record<keyof T, z.ZodTypeAny> = { ...shape };
+  const keys: (keyof T)[] = Object.keys(shape);
+  for (const k of keys) {
+    const v = looseValue(shape[k]);
+    newShape[k] = v;
+  }
+
+  return z.object(newShape).partial() as never;
+}
-- 
GitLab