Break parser handling out of main card.
This commit is contained in:
+2
-63
@@ -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')}. `;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -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']),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user