Skip to content

Commit

Permalink
Merge pull request #9 from open-audio-stack/feature/validation
Browse files Browse the repository at this point in the history
Zod validation
  • Loading branch information
kmturley authored Dec 1, 2024
2 parents cef111a + c3598e0 commit 67b40d5
Show file tree
Hide file tree
Showing 5 changed files with 82 additions and 66 deletions.
12 changes: 11 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
93 changes: 48 additions & 45 deletions src/helpers/package.ts
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -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;
}
14 changes: 7 additions & 7 deletions tests/data/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
},
Expand All @@ -39,19 +39,19 @@ 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',
},
{
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',
},
Expand Down
26 changes: 14 additions & 12 deletions tests/helpers/package.test.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -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',
},
]);
});
Expand All @@ -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',
},
]);
});

0 comments on commit 67b40d5

Please sign in to comment.