diff --git a/package-lock.json b/package-lock.json index 65c52d1..9db28a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,8 @@ "fs-extra": "^11.2.0", "glob": "^11.0.0", "js-yaml": "^4.1.0", - "semver": "^7.6.3" + "semver": "^7.6.3", + "zod": "^3.23.8" }, "devDependencies": { "@eslint/js": "^9.12.0", @@ -4096,6 +4097,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index e6d42e5..73ac136 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,8 @@ "fs-extra": "^11.2.0", "glob": "^11.0.0", "js-yaml": "^4.1.0", - "semver": "^7.6.3" + "semver": "^7.6.3", + "zod": "^3.23.8" }, "repository": { "type": "git", diff --git a/src/helpers/package.ts b/src/helpers/package.ts index 3484300..1c23a4c 100644 --- a/src/helpers/package.ts +++ b/src/helpers/package.ts @@ -1,12 +1,52 @@ -import { PluginFile, PresetFile, ProjectFile } from '../index-browser.js'; +import { z } from 'zod'; +import { Architecture } from '../types/Architecture.js'; +import { FileFormat } from '../types/FileFormat.js'; +import { FileType } from '../types/FileType.js'; import { License } from '../types/License.js'; -import { - PackageValidation, - PackageValidationError, - PackageValidationField, - PackageValidationRec, - PackageVersionType, -} from '../types/Package.js'; +import { PluginFile } from '../types/Plugin.js'; +import { PluginType } from '../types/PluginType.js'; +import { PresetFile } from '../types/Preset.js'; +import { PresetType } from '../types/PresetType.js'; +import { ProjectFile } from '../types/Project.js'; +import { ProjectType } from '../types/ProjectType.js'; +import { SystemType } from '../types/SystemType.js'; +import { PackageValidationRec, PackageVersionType } from '../types/Package.js'; + +// This is a first version using zod library for validation. +// If it works well, consider updating all types to infer from Zod objects. +// This will remove duplicatation of code between types and validators. + +export const PackageSystemValidator = z.object({ + max: z.number().min(0).max(99).optional(), + min: z.number().min(0).max(99).optional(), + type: z.nativeEnum(SystemType), +}); + +export const PackageFileValidator = z.object({ + architectures: z.nativeEnum(Architecture).array(), + format: z.nativeEnum(FileFormat), + sha256: z.string().length(64), + size: z.number().min(0).max(9999999999), + systems: PackageSystemValidator.array(), + type: z.nativeEnum(FileType), + url: z.string().min(0).max(256).startsWith('https://'), +}); + +export const PackageTypeObj = { ...PluginType, ...PresetType, ...ProjectType }; +export const PackageVersionValidator = z.object({ + audio: z.string().min(0).max(256).startsWith('https://'), + author: z.string().min(0).max(256), + changes: z.string().min(0).max(256), + date: z.string().datetime(), + description: z.string().min(0).max(256), + files: z.array(PackageFileValidator), + image: z.string().min(0).max(256).startsWith('https://'), + license: z.nativeEnum(License), + name: z.string().min(0).max(256), + tags: z.string().min(0).max(256).array(), + type: z.nativeEnum(PackageTypeObj), + url: z.string().min(0).max(256).startsWith('https://'), +}); // TODO refactor all this using a proper validation library. export function packageRecommendations(pkgVersion: PackageVersionType) { @@ -82,40 +122,3 @@ export function packageRecommendationsUrl( }); } } - -export function packageValidate(pkgVersion: PackageVersionType) { - const fields: PackageValidationField[] = [ - { name: 'audio', type: 'string' }, - { name: 'author', type: 'string' }, - { name: 'changes', type: 'string' }, - { name: 'date', type: 'string' }, - { name: 'description', type: 'string' }, - { name: 'files', type: 'object' }, - { name: 'image', type: 'string' }, - { name: 'license', type: 'string' }, - { name: 'name', type: 'string' }, - { name: 'tags', type: 'object' }, - { name: 'type', type: 'string' }, - { name: 'url', type: 'string' }, - ]; - const errors: PackageValidationError[] = []; - fields.forEach((field: PackageValidationField) => { - const versionField = pkgVersion[field.name as keyof PackageVersionType]; - if (versionField === undefined) { - errors.push({ - field: field.name, - error: PackageValidation.MISSING_FIELD, - valueExpected: field.type, - valueReceived: 'undefined', - }); - } else if (typeof versionField !== field.type) { - errors.push({ - field: field.name, - error: PackageValidation.INVALID_TYPE, - valueExpected: field.type, - valueReceived: typeof versionField, - }); - } - }); - return errors; -} diff --git a/tests/data/Plugin.ts b/tests/data/Plugin.ts index f0bb7ca..e05a3a5 100644 --- a/tests/data/Plugin.ts +++ b/tests/data/Plugin.ts @@ -23,10 +23,10 @@ export const PLUGIN: PluginInterface = { PluginFormat.LADSPAVersion2, PluginFormat.VST3, ], - format: FileFormat.Zip, - sha256: '42ad977d43d6caa75361cd2ad8794e36', + format: FileFormat.Tarball, + sha256: '8c2de75617e4e5b1051924d42b3b0306e5d1c3fea12c51c87d8505ba7857ca51', systems: [{ type: SystemType.Linux }], - size: 94448096, + size: 97294804, type: FileType.Archive, url: 'https://github.com/surge-synthesizer/releases-xt/releases/download/1.3.1/surge-xt-linux-1.3.1-pluginsonly.tar.gz', }, @@ -39,9 +39,9 @@ export const PLUGIN: PluginInterface = { PluginFormat.VST3, ], format: FileFormat.Zip, - sha256: 'd6bdab79c89f290e52222481b734650c', + sha256: 'b453e32c28594c20dcb059e2ca8ec3dc01c3f844500d5519680c15c3e7a262ea', systems: [{ type: SystemType.Macintosh }], - size: 180726292, + size: 186958216, type: FileType.Archive, url: 'https://github.com/surge-synthesizer/releases-xt/releases/download/1.3.1/surge-xt-macos-1.3.1-pluginsonly.zip', }, @@ -49,9 +49,9 @@ export const PLUGIN: PluginInterface = { architectures: [Architecture.X64], contains: [PluginFormat.WinStandalone, PluginFormat.CleverAudioPlugin, PluginFormat.VST3], format: FileFormat.Zip, - sha256: '415e08c30f275f212149b5351e651e65', + sha256: '9b9d3032985085dd662e06c4318c799919bcec0b4082a6afb2562c5538cf0853', systems: [{ type: SystemType.Windows }], - size: 48165645, + size: 49093875, type: FileType.Archive, url: 'https://github.com/surge-synthesizer/releases-xt/releases/download/1.3.1/surge-xt-win64-1.3.1-pluginsonly.zip', }, diff --git a/tests/helpers/package.test.ts b/tests/helpers/package.test.ts index bceef93..6d00c29 100644 --- a/tests/helpers/package.test.ts +++ b/tests/helpers/package.test.ts @@ -1,5 +1,5 @@ import { expect, test } from 'vitest'; -import { packageRecommendations, packageValidate } from '../../src/helpers/package.js'; +import { packageRecommendations, PackageVersionValidator } from '../../src/helpers/package.js'; import { PLUGIN } from '../data/Plugin'; import { PackageVersionType } from '../../src/types/Package'; @@ -19,19 +19,20 @@ test('Package recommendations fail', () => { }); test('Package validate pass', () => { - expect(packageValidate(PLUGIN)).toEqual([]); + expect(PackageVersionValidator.safeParse(PLUGIN).success).toEqual(true); }); test('Package validate missing field', () => { const PLUGIN_BAD: PackageVersionType = structuredClone(PLUGIN); // @ts-expect-error this is intentionally bad data. delete PLUGIN_BAD['audio']; - expect(packageValidate(PLUGIN_BAD)).toEqual([ + expect(PackageVersionValidator.safeParse(PLUGIN_BAD).error?.issues).toEqual([ { - error: 'missing-field', - field: 'audio', - valueExpected: 'string', - valueReceived: 'undefined', + code: 'invalid_type', + expected: 'string', + message: 'Required', + path: ['audio'], + received: 'undefined', }, ]); }); @@ -40,12 +41,13 @@ test('Package validate invalid type', () => { const PLUGIN_BAD: PackageVersionType = structuredClone(PLUGIN); // @ts-expect-error this is intentionally bad data. PLUGIN_BAD['audio'] = 123; - expect(packageValidate(PLUGIN_BAD)).toEqual([ + expect(PackageVersionValidator.safeParse(PLUGIN_BAD).error?.issues).toEqual([ { - error: 'invalid-type', - field: 'audio', - valueExpected: 'string', - valueReceived: 'number', + code: 'invalid_type', + expected: 'string', + message: 'Expected string, received number', + path: ['audio'], + received: 'number', }, ]); });