diff --git a/package.json b/package.json index b8eb4e09..8cad06ef 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "home-assistant-js-websocket": "^5.11.1", "lit": "^2.0.2", "screenfull": "^5.1.0", - "zod": "^3.10.0-beta.1" + "zod": "^3.10.0" }, "devDependencies": { "@babel/core": "^7.15.8", diff --git a/src/card.ts b/src/card.ts index 27cc6ce1..0e6afb46 100644 --- a/src/card.ts +++ b/src/card.ts @@ -18,6 +18,7 @@ import { handleAction, } from 'custom-card-helpers'; import screenfull from 'screenfull'; +import { z } from 'zod'; import { entitySchema, frigateCardConfigSchema } from './types'; import type { @@ -271,6 +272,42 @@ export class FrigateCard extends LitElement { return null; } + protected _getParseErrorPaths(error: z.ZodError): string[] { + /* 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 human-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. */ + let contenders: 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.length) { + contenders = contenders.concat(nestedErrors); + } + } + } else if (issue.code == 'invalid_type') { + if (issue.path[issue.path.length - 1] == 'type') { + return []; + } + contenders.push(this._getParseErrorPathString(issue.path)); + } + } + } + return contenders; + } + + // Convert an array of strings and indices into a more human readable string, + // e.g. [a, 1, b, 2] => 'a[1] -> b[2]' protected _getParseErrorPathString(path: (string | number)[]): string { let out = ''; for (let i = 0; i < path.length; i++) { @@ -294,12 +331,12 @@ export class FrigateCard extends LitElement { const parseResult = frigateCardConfigSchema.safeParse(inputConfig); if (!parseResult.success) { - let hint = ''; - if (parseResult.error && parseResult.error.issues) { - hint = this._getParseErrorPathString(parseResult.error.issues[0].path); - } + const hint = this._getParseErrorPaths(parseResult.error); throw new Error( - localize('error.invalid_configuration') + (hint ? `: ${hint}` : ''), + `${localize('error.invalid_configuration')}: ` + + (hint.length + ? JSON.stringify(hint, null, ' ') + : localize('error.invalid_configuration_no_hint')), ); } const config = parseResult.data; diff --git a/src/localize/languages/en.json b/src/localize/languages/en.json index 71e563cf..d7126823 100644 --- a/src/localize/languages/en.json +++ b/src/localize/languages/en.json @@ -97,6 +97,7 @@ "could_not_resolve": "Could not resolve media URL", "no_live_camera": "The camera_entity parameter must be set and valid for this live provider", "invalid_configuration": "Invalid configuration", + "invalid_configuration_no_hint": "No location hint available (bad or missing type?)", "missing_webrtc": "WebRTC component not found", "no_frigate_camera_name": "Cannot autodetect Frigate camera name, you need to either set camera_entity and / or frigate_camera_name", "could_not_render_elements": "Could not render picture elements", diff --git a/src/types.ts b/src/types.ts index 741ea4f1..b07d1028 100644 --- a/src/types.ts +++ b/src/types.ts @@ -195,7 +195,15 @@ const conditionalSchema = z.object({ // https://www.home-assistant.io/lovelace/picture-elements/#custom-elements const customSchema = z.object({ // Insist that Frigate card custom elements are handled by other schemas. - type: z.string().regex(/^custom:(?!frigate-card).+/), + type: z.string().superRefine((val, ctx) => { + if (!val.match(/^custom:(?!frigate-card).+/)) { + ctx.addIssue({ + code: z.ZodIssueCode.invalid_type, + expected: "string", + received: "string", + }); + } + }) }).passthrough(); /**