Break parser handling out of main card.

This commit is contained in:
Dermot Duffy
2023-05-20 20:23:39 -07:00
parent 1c5e11009f
commit d414540b7c
3 changed files with 155 additions and 63 deletions
+2 -63
View File
@@ -18,7 +18,6 @@ import throttle from 'lodash-es/throttle';
import screenfull from 'screenfull'; import screenfull from 'screenfull';
import { ViewContext } from 'view'; import { ViewContext } from 'view';
import 'web-dialog'; import 'web-dialog';
import { z } from 'zod';
import pkg from '../package.json'; import pkg from '../package.json';
import { actionHandler } from './action-handler-directive.js'; import { actionHandler } from './action-handler-directive.js';
import { AutomationsController } from './automations'; import { AutomationsController } from './automations';
@@ -94,6 +93,7 @@ import {
createViewWithSelectedSubstream, createViewWithSelectedSubstream,
} from './utils/substream'; } from './utils/substream';
import { Timer } from './utils/timer'; import { Timer } from './utils/timer';
import { getParseErrorPaths } from './utils/zod.js';
import { View } from './view/view.js'; import { View } from './view/view.js';
/** A note on media callbacks: /** A note on media callbacks:
@@ -341,67 +341,6 @@ class FrigateCard extends LitElement {
return this._cameraManager.getStore().getCameraConfig(this._view.camera); return this._cameraManager.getStore().getCameraConfig(this._view.camera);
} }
/**
* Get configuration parse errors.
* @param error The ZodError object from parsing.
* @returns An array of string error paths.
*/
protected _getParseErrorPaths<T>(error: z.ZodError<T>): Set<string> | null {
/* Zod errors involving unions are complex, as Zod may not be able to tell
* where the 'real' error is vs simply a union option not matching. This
* function finds all ZodError "issues" that don't have an error with 'type'
* in that object ('type' is the union discriminator for picture elements,
* the major union in the schema). An array of user-readable error
* locations is returned, or an empty list if none is available. None being
* available suggests the configuration has an error, but we can't tell
* exactly why (or rather Zod simply says it doesn't match any of the
* available unions). This usually suggests the user specified an incorrect
* type name entirely. */
const contenders = new Set<string>();
if (error && error.issues) {
for (let i = 0; i < error.issues.length; i++) {
const issue = error.issues[i];
if (issue.code == 'invalid_union') {
const unionErrors = (issue as z.ZodInvalidUnionIssue).unionErrors;
for (let j = 0; j < unionErrors.length; j++) {
const nestedErrors = this._getParseErrorPaths(unionErrors[j]);
if (nestedErrors && nestedErrors.size) {
nestedErrors.forEach(contenders.add, contenders);
}
}
} else if (issue.code == 'invalid_type') {
if (issue.path[issue.path.length - 1] == 'type') {
return null;
}
contenders.add(this._getParseErrorPathString(issue.path));
} else if (issue.code != 'custom') {
contenders.add(this._getParseErrorPathString(issue.path));
}
}
}
return contenders;
}
/**
* Convert an array of strings and indices into a more user readable string,
* e.g. [a, 1, b, 2] => 'a[1] -> b[2]'
* @param path An array of strings and numbers.
* @returns A single string.
*/
protected _getParseErrorPathString(path: (string | number)[]): string {
let out = '';
for (let i = 0; i < path.length; i++) {
const item = path[i];
if (typeof item == 'number') {
out += '[' + item + ']';
} else if (out) {
out += ' -> ' + item;
} else {
out = item;
}
}
return out;
}
/** /**
* Set the card configuration. * Set the card configuration.
@@ -415,7 +354,7 @@ class FrigateCard extends LitElement {
const parseResult = frigateCardConfigSchema.safeParse(inputConfig); const parseResult = frigateCardConfigSchema.safeParse(inputConfig);
if (!parseResult.success) { if (!parseResult.success) {
const configUpgradeable = isConfigUpgradeable(inputConfig); const configUpgradeable = isConfigUpgradeable(inputConfig);
const hint = this._getParseErrorPaths(parseResult.error); const hint = getParseErrorPaths(parseResult.error);
let upgradeMessage = ''; let upgradeMessage = '';
if (configUpgradeable && getLovelace().mode !== 'yaml') { if (configUpgradeable && getLovelace().mode !== 'yaml') {
upgradeMessage = `${localize('error.upgrade_available')}. `; upgradeMessage = `${localize('error.upgrade_available')}. `;
+62
View File
@@ -56,3 +56,65 @@ export function getParseErrorKeys<T>(error: z.ZodError<T>): string[] {
const errors = error.format(); const errors = error.format();
return Object.keys(errors).filter((v) => !v.startsWith('_')); return Object.keys(errors).filter((v) => !v.startsWith('_'));
} }
/**
* Get configuration parse errors.
* @param error The ZodError object from parsing.
* @returns An array of string error paths.
*/
export const getParseErrorPaths = <T>(error: z.ZodError<T>): Set<string> | null => {
/* Zod errors involving unions are complex, as Zod may not be able to tell
* where the 'real' error is vs simply a union option not matching. This
* function finds all ZodError "issues" that don't have an error with 'type'
* in that object ('type' is the union discriminator for picture elements,
* the major union in the schema). An array of user-readable error
* locations is returned, or an empty list if none is available. None being
* available suggests the configuration has an error, but we can't tell
* exactly why (or rather Zod simply says it doesn't match any of the
* available unions). This usually suggests the user specified an incorrect
* type name entirely. */
const contenders = new Set<string>();
if (error && error.issues) {
for (let i = 0; i < error.issues.length; i++) {
const issue = error.issues[i];
if (issue.code == 'invalid_union') {
const unionErrors = (issue as z.ZodInvalidUnionIssue).unionErrors;
for (let j = 0; j < unionErrors.length; j++) {
const nestedErrors = getParseErrorPaths(unionErrors[j]);
if (nestedErrors && nestedErrors.size) {
nestedErrors.forEach(contenders.add, contenders);
}
}
} else if (issue.code == 'invalid_type') {
if (issue.path[issue.path.length - 1] == 'type') {
return null;
}
contenders.add(getParseErrorPathString(issue.path));
} else if (issue.code != 'custom') {
contenders.add(getParseErrorPathString(issue.path));
}
}
}
return contenders;
};
/**
* Convert an array of strings and indices into a more user readable string,
* e.g. [a, 1, b, 2] => 'a[1] -> b[2]'
* @param path An array of strings and numbers.
* @returns A single string.
*/
const getParseErrorPathString = (path: (string | number)[]): string => {
let out = '';
for (let i = 0; i < path.length; i++) {
const item = path[i];
if (typeof item == 'number') {
out += '[' + item + ']';
} else if (out) {
out += ' -> ' + item;
} else {
out = item;
}
}
return out;
};
+91
View File
@@ -0,0 +1,91 @@
import { describe, expect, it } from 'vitest';
import { z } from 'zod';
import {
deepRemoveDefaults,
getParseErrorKeys,
getParseErrorPaths,
} from '../../src/utils/zod';
describe('deepRemoveDefaults', () => {
it('should remove string defaults', () => {
const schema = z.object({
string: z.string().default('foo'),
});
const result = deepRemoveDefaults(schema).parse({});
expect(result.string).toBeUndefined();
});
it('should remove array defaults', () => {
const schema = z.object({
array: z.string().array().default(['foo']),
});
const result = deepRemoveDefaults(schema).parse({});
expect(result.array).toBeUndefined();
});
it('should remove optional defaults', () => {
const schema = z.object({
string: z.string().default('foo').optional(),
});
const result = deepRemoveDefaults(schema).parse({});
expect(result.string).toBeUndefined();
});
it('should remove null defaults', () => {
const schema = z.object({
null: z.string().default('foo').nullable(),
});
const result = deepRemoveDefaults(schema).parse({});
expect(result.null).toBeUndefined();
});
it('should remove null defaults', () => {
const schema = z.object({
tuple: z.tuple([z.string()]).default(['foo']),
});
const result = deepRemoveDefaults(schema).parse({});
expect(result.tuple).toBeUndefined();
});
it('should not interfere with parsing', () => {
const schema = z.object({
string: z.string().default('foo'),
});
const result = deepRemoveDefaults(schema).parse({ string: 'moo' });
expect(result.string).toBe('moo');
});
});
describe('getParseErrorKeys', () => {
it('should get error keys', () => {
const result = z.object({ required: z.string() }).safeParse({});
expect(result.success).toBeFalsy();
if (result.success) {
return;
}
expect(getParseErrorKeys(result.error)).toEqual(['required']);
});
});
describe('getParseErrorPaths', () => {
it('should get simple error paths', () => {
const result = z.object({ required: z.string() }).safeParse({});
expect(result.success).toBeFalsy();
if (result.success) {
return;
}
expect(getParseErrorPaths(result.error)).toEqual(new Set(['required']));
});
it('should get union error paths', () => {
const type_one = z.object({ type: z.string(), data: z.string() });
const type_two = z.object({ type: z.literal('two'), data: z.string() });
const schema = z.object({
array: type_one.or(type_two).array(),
});
const result = schema.safeParse({ array: [{}] });
expect(result.success).toBeFalsy();
if (result.success) {
return;
}
expect(getParseErrorPaths(result.error)).toEqual(
new Set(['array[0] -> type', 'array[0] -> data']),
);
});
});